agql-auth issues local access tokens containing local authorization data. OIDC
or Microsoft claims may influence roles and scopes during provisioning, but
GraphQL authorization should use the local AuthUser attached to the request.
Local access tokens preserve this claim shape:
sub: local user IDsid: local session IDroles: local rolesscope: local scopes as one space-delimited OAuth stringctx: typedSessionContextiss: configured issueraud: configured audienceexp: expiry timestampiat: issued-at timestamp
The ctx claim contains the local authentication method, MFA state, and
optional active scope.
After validation, scope is normalized into the public
AuthUser::scopes: Vec<String>. This in-process representation and all guard
APIs are unchanged. During a bounded migration, validators can also read the
pre-0.14 scopes array; see
Access-token scope claims.
SessionContext.auth_method records how the local session was issued:
PasswordEmailCodeSmsCodeTotpStepUpServiceTokenOidcMicrosoftOidc
For external login, Microsoft or OIDC tokens are used only to verify the login event. The resulting GraphQL request is authorized with local roles, scopes, and session context.
AuthUser remains the user-session type. For code that can accept either user
sessions or API/service tokens, use AuthPrincipal:
AuthPrincipal::User(AuthUser)AuthPrincipal::ApiToken(ApiTokenPrincipal)
AuthPrincipal exposes subject(), roles(), scopes(), scope helper
methods, and API-token accessors for audience, resource binding, token ID, and
expiry. API-token principals return an empty role list unless the host models
roles separately.
Direct scope helpers remain exact string matching. This preserves existing runtime behavior:
use agql_auth::{has_all_scopes, has_any_scope, has_scope};
assert!(has_scope(&user.scopes, "orders.read"));
assert!(has_any_scope(&user.scopes, &["orders.read", "orders.write"]));
assert!(has_all_scopes(&user.scopes, &["orders.read", "profile.read"]));AuthUser exposes the same checks:
if user.has_scope("orders.read") {
// ...
}For wildcard or hierarchical scopes, configure a ScopeMatch implementation
and inject it through AuthRuntime. Guards read the runtime matcher and fall
back to exact matching when no runtime is present:
use std::sync::Arc;
use agql_auth::{AuthRuntime, HierarchicalScopeMatch};
let request = request.data(AuthRuntime::new(Arc::new(
HierarchicalScopeMatch::with_defaults(),
)));AuthService, AccessTokenValidator, and CombinedAuth inject
AuthRuntime after successful authentication. See
Scope matching for the normative algorithm and golden
vectors.
Use guards when a resolver has a fixed authorization requirement:
use agql_auth::{
RequireAllRoles, RequireAllScopes, RequireAnyRole, RequireAnyScope, RequireAuth, RequireScope,
};
#[Object]
impl Query {
#[graphql(guard = "RequireAuth::new()")]
async fn viewer(&self) -> Viewer {
// ...
}
#[graphql(guard = "RequireAnyRole::new([\"Admin\", \"Operator\"])")]
async fn admin_view(&self) -> AdminView {
// ...
}
#[graphql(guard = "RequireScope::new(\"orders.read\")")]
async fn orders(&self) -> Vec<Order> {
// ...
}
#[graphql(guard = "RequireAllScopes::new([\"orders.read\", \"profile.read\"])")]
async fn account_orders(&self) -> Vec<Order> {
// ...
}
}Use resolver code when authorization depends on object ownership, tenant membership, or other dynamic data.
For resolvers that accept either a user session or an API token, use the generic
principal guards. CombinedAuth can inject either credential type into one
AuthPrincipal:
use agql_auth::{
CombinedAuth, RequireAllPrincipalScopes, RequireAnyPrincipalScope,
RequirePrincipal, RequirePrincipalScope,
};
let request = CombinedAuth::new(&access_token_validator, &api_token_service)
.inject_http_auth(request, authorization_header, metadata)
.await?;
#[Object]
impl Query {
#[graphql(guard = "RequirePrincipal::new()")]
async fn viewer(&self) -> Viewer {
// ...
}
#[graphql(guard = "RequirePrincipalScope::new(\"inventory.read\")")]
async fn inventory(&self) -> Vec<Item> {
// ...
}
}inject_http_auth and authenticate_connection_init_value attach AuthUser to
the async-graphql request data. Resolvers can read it with:
use agql_auth::{auth_user_from_ctx, auth_user_from_ctx_opt};
let required_user = auth_user_from_ctx(ctx)?;
let optional_user = auth_user_from_ctx_opt(ctx);auth_user_from_ctx returns an authentication error when no user is present.
For generic user-or-token access, use:
use agql_auth::{principal_from_ctx, principal_from_ctx_opt};
let principal = principal_from_ctx(ctx)?;
let optional_principal = principal_from_ctx_opt(ctx);Hosts that verify a channel outside this crate can inject ChannelIdentity.
The crate does not parse certificates or verify channel credentials.
use agql_auth::{ChannelIdentity, RequireChannelScheme};
let request = request.data(
ChannelIdentity::new("mtls", "device-1").with_claim("fingerprint", "sha256:..."),
);
#[Object]
impl Mutation {
#[graphql(guard = "RequireChannelScheme::new(\"mtls\")")]
async fn device_action(&self) -> bool {
true
}
}