// The enforcement half of the security schemes. `security_scheme.mbt` describes
// a scheme to clients; this is what stands in front of a route that names it —
// FastAPI's `Security(APIKeyHeader(...))`, `Security(HTTPBasic())` and friends,
// which are a declaration and a guard at once. Every guard here answers the same
// question the OAuth2 one does: given a request, the scopes the route asked for
// and the verification time, is this caller admitted, and if not, with what.
//
// `auto_error` is FastAPI's, and means the same: with it off, a missing or bad
// credential does not refuse the request — the guard yields an anonymous caller
// and the route decides for itself, reading the credential through the
// `Context` extractors.
///|
/// Declare an API-key scheme under `name` and wire it as a runtime enforcer (←
/// FastAPI's `Security(APIKeyHeader(name=...))`). The scheme appears in the
/// document as `apiKey` in the header, query or cookie the `key` names, and a
/// route requiring `name` is refused with `403` unless it presents a key the
/// scheme accepts.
pub fn App::secure_api_key(
self : App,
name : String,
key : @security.ApiKey,
description~ : String = "",
auto_error~ : Bool = true,
) -> Unit {
self.security_schemes.push({ name, kind: key.scheme(), description, })
self.enforcers[name] = (ctx, _scopes, _now) => {
verify_api_key(key, ctx, auto_error)
}
}
///|
/// Declare an HTTP Basic scheme under `name` and wire it as a runtime enforcer
/// (← `Security(HTTPBasic())`). A route requiring `name` is refused with `401`
/// and a `Basic` challenge unless it presents credentials the scheme accepts.
pub fn App::secure_basic(
self : App,
name : String,
basic : @security.BasicAuth,
description~ : String = "",
auto_error~ : Bool = true,
) -> Unit {
self.security_schemes.push({ name, kind: @security.HttpBasic, description, })
self.enforcers[name] = (ctx, _scopes, _now) => {
verify_basic(basic, ctx, auto_error)
}
}
///|
/// Declare an HTTP Digest scheme under `name` and wire it as a runtime enforcer
/// (← `Security(HTTPDigest())`). A route requiring `name` is refused with `403`
/// unless it presents a `Digest` credential the scheme accepts.
pub fn App::secure_digest(
self : App,
name : String,
digest : @security.DigestAuth,
description~ : String = "",
auto_error~ : Bool = true,
) -> Unit {
self.security_schemes.push({ name, kind: @security.HttpDigest, description, })
self.enforcers[name] = (ctx, _scopes, _now) => {
verify_digest(digest, ctx, auto_error)
}
}
///|
/// Declare a plain HTTP bearer scheme under `name` and wire it as a runtime
/// enforcer (← `Security(HTTPBearer())`). The token is verified as an HS256 JWT
/// against `secret` at the app clock's time, and the route's required scopes are
/// checked against the token's — the same guard the OAuth2 flows use, without an
/// OAuth2 flow to advertise.
pub fn App::secure_bearer(
self : App,
name : String,
secret : String,
bearer_format~ : String = "JWT",
description~ : String = "",
auto_error~ : Bool = true,
) -> Unit {
self.security_schemes.push({
name,
kind: @security.HttpBearer(bearer_format~),
description,
})
self.enforcers[name] = (ctx, scopes, now) => {
@security.verify_bearer(ctx, secret, scopes, now, auto_error)
}
}
///|
/// Declare an OAuth2 authorization-code scheme under `name` and wire it as a
/// runtime enforcer (← `Security(OAuth2AuthorizationCodeBearer(...))`). The
/// document describes the browser redirect and token endpoints and the
/// advertised `scopes`; the guard verifies the presented bearer token exactly as
/// the password flow's does, since by the time a request arrives the two flows
/// have produced the same credential.
pub fn App::secure_oauth2_code(
self : App,
name : String,
code : @security.OAuth2CodeBearer,
scopes~ : Array[(String, String)] = [],
description~ : String = "",
auto_error~ : Bool = true,
) -> Unit {
self.security_schemes.push({ name, kind: code.scheme(scopes~), description, })
self.enforcers[name] = (ctx, required, now) => {
@security.verify_bearer(ctx, code.secret, required, now, auto_error)
}
}
///|
/// Declare an OpenID Connect scheme under `name` and wire it as a runtime
/// enforcer (← `Security(OpenIdConnect(openIdConnectUrl=...))`). `url` is the
/// discovery document a client reads every other parameter from; `secret` is
/// what the ID token presented as a bearer credential is verified against.
///
/// Swagger 2.0 has no `openIdConnect` type, so this scheme is absent from a 2.0
/// document rather than described as something it is not.
pub fn App::secure_openid(
self : App,
name : String,
url : String,
secret : String,
description~ : String = "",
auto_error~ : Bool = true,
) -> Unit {
self.security_schemes.push({
name,
kind: @security.OpenIdConnect(url~),
description,
})
self.enforcers[name] = (ctx, scopes, now) => {
@security.verify_bearer(ctx, secret, scopes, now, auto_error)
}
}
///|
/// The guard behind a declared API-key scheme.
fn verify_api_key(
key : @security.ApiKey,
ctx : @ctx.Context,
auto_error : Bool,
) -> Result[@security.AuthenticatedUser, @moonasgi.Response] {
match key.read(ctx) {
None =>
@security.refuse(auto_error, () => api_key_denied("Not authenticated"))
Some(presented) =>
if (key.verify)(presented) {
// The key *is* the identity here, exactly as FastAPI's `APIKeyHeader`
// dependency yields the key itself to the route.
Ok({ subject: presented, scopes: [], claims: Map([]), })
} else {
@security.refuse(auto_error, () => {
api_key_denied("Invalid authentication credentials")
})
}
}
}
// -- HTTP basic and digest ----------------------------------------------------
///|
/// The guard behind a declared HTTP Basic scheme.
fn verify_basic(
basic : @security.BasicAuth,
ctx : @ctx.Context,
auto_error : Bool,
) -> Result[@security.AuthenticatedUser, @moonasgi.Response] {
match ctx.http_basic() {
None =>
@security.refuse(auto_error, () => {
@security.unauthorized("Not authenticated", challenge=basic.challenge())
})
Some(cred) =>
if (basic.verify)(cred) {
Ok({ subject: cred.username, scopes: [], claims: Map([]), })
} else {
@security.refuse(auto_error, () => {
@security.unauthorized(
"Incorrect username or password",
challenge=basic.challenge(),
)
})
}
}
}
///|
/// The guard behind a declared HTTP Digest scheme. Like FastAPI's `HTTPDigest`
/// it answers `403`, since a digest challenge a server cannot yet build a nonce
/// for is no challenge at all.
fn verify_digest(
digest : @security.DigestAuth,
ctx : @ctx.Context,
auto_error : Bool,
) -> Result[@security.AuthenticatedUser, @moonasgi.Response] {
match ctx.digest_credentials() {
None =>
@security.refuse(auto_error, () => {
@security.forbidden("Not authenticated")
})
Some(cred) =>
if (digest.verify)(cred) {
Ok(@security.anonymous())
} else {
@security.refuse(auto_error, () => {
@security.forbidden("Invalid authentication credentials")
})
}
}
}
// -- wiring a scheme onto an app ----------------------------------------------
///|
/// Refuse a request that presented no acceptable API key. FastAPI's API-key
/// classes answer `403`, not `401`, because an `apiKey` scheme has no
/// `WWW-Authenticate` challenge to offer and a `401` without one is malformed.
fn api_key_denied(detail : String) -> @moonasgi.Response {
@security.forbidden(detail)
}