📖 4 min read (~ 800 words).

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 managers

Full 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:

$ cd hack/tools && go run . gen-tokens        # RSA keypair + role JWTs
$ ./composed-auth/exerciser.sh                # sends a sequence of curl requests