Composed auth
Real APIs rarely have a single security scheme. The composed-auth example mixes four — basic auth, an API key (by header or query), and scoped JWT tokens — and composes them per operation with AND and OR semantics. It’s the reference for anything beyond a single authenticator.
Tip
Source: composed-auth/.
Generated with swagger generate server --name multi-auth-example --spec ./swagger.yml --principal models.Principal.
The restapi/configure_*.go and auth/authorizers.go files are hand-written.
Four schemes
The spec declares basic auth (isRegistered), an API key in either the header
(isReseller) or a query param (isResellerQuery), and a scoped oauth2-typed
scheme (hasRole) used purely to carry JWT scopes — go-swagger does not run an
OAuth2 flow here, it just extracts the required scopes and hands them to your
authorizer:
isRegistered:
# This scheme uses the header: "Authorization: Basic {base64 encoded string defined by username:password}"
# Scopes are not supported with this type of authorization.
type: basic
isReseller:
# This scheme uses the header: "X-Custom-Key: {base64 encoded string}"
# Scopes are not supported with this type of authorization.
type: apiKey
in: header
name: X-Custom-Key
isResellerQuery:
# This scheme uses the query parameter "CustomKeyAsQuery"
# Scopes are not supported with this type of authorization.
type: apiKey
in: query
name: CustomKeyAsQuery
hasRole:
# This scheme uses the header: "Authorization: Bearer {base64 encoded string representing a JWT}"
# Alternatively, the query param: "access_token" may be used.
#
# In our scenario, we must use the query param version in order to avoid
# passing several headers with key 'Authorization'
type: oauth2
# The flow and URLs in spec are for documentary purpose: go-swagger does not implement OAuth workflows
flow: accessCode
authorizationUrl: 'https://dummy.oauth.net/auth'
tokenUrl: 'https://dumy.oauth.net/token'
# Required scopes are passed by the runtime to the authorizer
scopes:
customer: scope of registered customers
inventoryManager: scope of resellers acting as inventory managersFull source: composed-auth/swagger.yml
Composing requirements with AND / OR
A security block on an operation is a list of alternatives (OR), and each
alternative is a map of schemes that must all pass (AND). So /order/add
accepts a registered customer, or a reseller (by header), or a reseller (by
query) — each combined with the right JWT role:
security:
- isRegistered: []
hasRole: [ customer ]
- isReseller: []
hasRole: [ inventoryManager ]
- isResellerQuery: []
hasRole: [ inventoryManager ]Full source: composed-auth/swagger.yml
An empty security: [] on an operation opts it out entirely (public), overriding
the spec’s top-level default.
The authorizers
Each scheme maps to a hook whose signature depends on its type: basic auth gets
(user, pass), an API key gets (token), and a scoped scheme gets
(token, scopes). The example delegates each to a function in
auth/authorizers.go:
api.HasRoleAuth = func(token string, scopes []string) (*models.Principal, error) {
// The header: Authorization: Bearer {base64 string} (or ?access_token={base 64 string} param) has already
// been decoded by the runtime as a token
api.Logger("HasRoleAuth handler called")
return auth.HasRole(token, scopes)
}
// Applies when the Authorization header is set with the Basic scheme
api.IsRegisteredAuth = func(user string, password string) (*models.Principal, error) {
// The header: Authorization: Basic {base64 string} has already been decoded by the runtime as a
// username:password pair
api.Logger("IsRegisteredAuth handler called")
return auth.IsRegistered(user, password)
}
// Applies when the "X-Custom-Key" header is set
api.IsResellerAuth = func(token string) (*models.Principal, error) {
api.Logger("IsResellerAuth handler called")
return auth.IsReseller(token)
}
// Applies when the "CustomKeyAsQuery" query is set
api.IsResellerQueryAuth = func(token string) (*models.Principal, error) {
api.Logger("ResellerQueryAuth handler called")
return auth.IsReseller(token)
}Full source: composed-auth/restapi/configure_multi_auth_example.go
The basic-auth authorizer is a plain credential check returning a typed principal:
// IsRegistered determines if the user is properly registered,
// i.e if a valid username:password pair has been provided.
func IsRegistered(user, pass string) (*models.Principal, error) {
password, ok := userDb[user]
if !ok || pass != password {
return nil, errors.New(401, "Unauthorized: not a registered user")
}
return &models.Principal{
Name: user,
}, nil
}Full source: composed-auth/auth/authorizers.go
The scoped HasRole authorizer goes further: it parses the JWT, then checks the
token’s claimed roles against the scopes the runtime passed in — the mechanism
that makes hasRole: [ customer ] in the spec actually mean something.
Trying it
Generate test keys and JWTs, then exercise the composed requirements:
Related
- Basic & API-key auth — the single-scheme starting point.
- OAuth2 access-code — a real OAuth2 handshake (vs. JWT-scope extraction here).