Class to request and cache tokens on client-side.
import {
TokenCache,
OAuthGrantType,
Token
} from 'authmosphere';
const options = {
cacheConfig: {
percentageLeft: 0.75
},
logger: someExternalLogger
};
const oAuthConfig = {
grantType: OAuthGrantType.CLIENT_CREDENTIALS_GRANT,
accessTokenEndpoint: 'https://example.com/access_token',
credentialsDir: './credentialsDir'
};
const tokenConfig = {
'service-foo': ['foo.read', 'foo.write'],
'service-bar': ['bar.read']
};
// create a new TokenCache instance
const tokenCache = new TokenCache(tokenConfig, oAuthConfig, options);
// request and resolve a token from the cache
tokenCache
.get('service-foo') // needs to match with a key from 'tokenConfig'
.then((token: Token) => {
// ...use the token...
});
Toggle description
constructor(tokenConfig, oauthConfig, options)
tokenConfig: [key: string]: string[]
- Mapping between a name (representing a token) and the scopes requested for the corresponding token.oauthConfig: TokenCacheOAuthConfig
- EitherClientCredentialsGrantConfig
orPasswordCredentialsGrantConfig
plus the additionaltokenInfoEndpoint: string
property that specifies the URL of the token validation endpoint.options?: TokenCacheOptions
cacheConfig?: CacheConfig
percentageLeft: number
- To determine when a token is expired locally (means when to issue a new token): if the token exists for((1 - percentageLeft) * lifetime)
then issue a new one.
logger?: Logger
Toggle description
Returns cached token or requests a new one if lifetime (as configured in cacheOptions.cacheConfig
) is expired.
get(tokenName) => Promise<Token>
tokenName: string
- Key of the token as configured intokenConfig
Promise<Token>
that resolves with a token with configured scopes. In case of error rejects with an error message.
Toggle description
Triggers the request of a new token. Invalidates the old cache entry.
refreshToken(tokenName: string) => Promise<Token>
tokenName: string
- Key of the token as configured intokenConfig
Promise<Token>
that resolves with a token with configured scopes. In case of error rejects with an error message.
Toggle description
Triggers the request of a new token for all configured ones. Invalidates all cache entries.
refreshAllTokens() => Promise<TokenMap>
Promise<TokenMap>
that resolves with a map of tokens with configured scopes. In case of error rejects with an error message.
This tooling provides helper functions to request and validate tokens based on the OAuth 2.0 RFC 6749 specification.
Toggle description
Requests a token based on the given configuration (which specifies the grant type and corresponding parameters). See the OAuthConfig
documentation for details.
import {
OAuthGrantType,
ClientCredentialsGrantConfig,
Token,
getAccessToken
} from 'authmosphere';
const config: ClientCredentialsGrantConfig = {
grantType: OAuthGrantType.CLIENT_CREDENTIALS_GRANT,
credentialsDir: './crendentials',
accessTokenEndpoint: 'https://example.com/token_validation',
scopes: ['my-app.read', 'my-app.write'];
};
getAccessToken(config)
.then((token: Token) => {
// ...use the token...
})
.catch((err) => {
// ...handle the error...
});
getAccessToken(config[, logger]) => Promise<Token>
config: OAuthConfig
- OAuth configuration for the request (specify grant type and corresponding parameters)logger?: Logger
Promise<Token>
which resolves with the token if the request was successful. Otherwise, rejects with an error message.
Toggle description
Requests validation information from the specified tokenInfoUrl
and returns a Promise
that resolves with these information, if the token is valid. Otherwise, it rejects with an error.
import {
Token,
getTokenInfo
} from 'authmosphere';
getTokenInfo('https://example.com/token_validation', '1234-5678-9000')
.then((token: Token) => {
// ...token is valid...
})
.catch((err) => {
// ...token is invalid...
})
getTokenInfo<T>(tokenInfoUrl, accessToken[, logger]): Promise<Token<T>>
tokenInfoUrl: string
- OAuth endpoint for validating tokensaccessToken: string
- access token to be validatedlogger?: Logger
Promise<Token>
which resolves with the validated token if it is valid. Otherwise, rejects with an error message.
Toggle description
Helper function to create the URI to request an authorization code when using the Authorization Code Grant.
const uri = createAuthCodeRequestUri('https://example.com/authorize', 'http://your-app.com/handle-auth-code', '1234-client-id');
createAuthCodeRequestUri(authorizationEndpoint, redirectUri, clientId[, queryParams]) => string
authorizationEndpoint: string
- OAuth authorization endpointredirectUri: string
- Absolute URI specifying the endpoint the authorization code is responded to (see OAuth 2.0 specification for details)clientId: string
- client id of the requesting applicationqueryParams?: { [index: string]: string }
- Set of key-value pairs which will be added as query parameters to the request (for example to addstate
orscopes
)
string
of the created request URI.
Authmosphere provides two middleware factories to secure Express based http services.
Toggle description
Middleware that handles OAuth authentication for API endpoints. It extracts and validates the access token from the request.
If configured as a global middleware (see usage section), all requests need to provide a valid token to access the endpoint.
If some endpoints should be excluded from this restriction, they need to be added to the options.publicEndpoints
array to be whitelisted.
If validation of the provided token fails the middleware rejects the request with status 401 UNAUTHORIZED.
To overwrite this behavior a custom handler can be specified by passing in options.onNotAuthenticatedHandler
(see onNotAuthenticatedHandler
).
⚠️ While this middleware could also be configured per endpoint (i.e.app.get(authenticationMiddleware(...), endpoint)
it is not recommended as using it as global middleware will force you into a whitelist setup.- Make sure
authenticationMiddleware
is at the top of the registered request handlers. This is essential to guarantee the enforceability of the whitelist strategy.
- Make sure
⚠️ The middleware attaches metadata (scopes of the token) to the express request object. TherequireScopesMiddleware
relies on this information.
import {
authenticationMiddleware
} from 'authmosphere';
app.use(authenticationMiddleware({
publicEndpoints: ['/heartbeat', '/status'],
tokenInfoEndpoint: 'https://example.com/token_validation'
});
authenticationMiddleware(options) => express.RequestHandler
options
:tokenInfoEndpoint: string
- URL of the Token validation endpointpublicEndpoints?: string[]
- List of whitelisted API pathslogger?: Logger
onNotAuthenticatedHandler?: onNotAuthenticatedHandler
- custom response handler
Toggle description
A factory that returns a middleware that compares scopes attached to express.Request
object with a given list (scopes
parameter). If all required scopes are matched, the middleware calls next
. Otherwise, it rejects the request with 403 FORBIDDEN.
⚠️ This middleware requires scope information to be attached to theExpress.request
object. TheauthenticationMiddleware
can do this job. Otherwiserequest.$$tokeninfo.scope: string[]
has to be set manually.
There may occur cases where another type of authorization should be used. For that cases options.precedenceFunction
has to be set. If the precedence
function resolves with anything else than 'true', normal scope validation is applied afterwards.
Detailed middleware authorization flow:
+-----------------------------------+
| is precedenceFunction defined? |
+-----------------------------------+
| |
| | yes
| v
| +----------------------+ resolve(true) +--------+ +---------------+
no | | precedenceFunction() |---------------->| next() | ----->| call endpoint |
| +----------------------+ +--------+ +---------------+
| |
| | reject
v v
+-----------------------------------+ yes +--------+ +---------------+
| scopes match with requiredScopes? |---------------->| next() |------>| call endpoint |
+-----------------------------------+ +--------+ +---------------+
|
no/ |
throw v
+----------------------------------+ yes +--------------------------------+
| is onAuthorizationFailedHandler |----------------->| onAuthorizationFailedHandler() |
| configured? | +--------------------------------+
+----------------------------------+
|
| no +--------------------------------+
+-------------------------------------------->| response.sendStatus(403) |
+--------------------------------+
import {
requireScopesMiddleware
} from 'authmosphere';
app.get('/secured/route', requireScopesMiddleware(['scopeA', 'scopeB']), (request, response) => {
// handle request
});
(scopes: string[], options?: ScopeMiddlewareOptions) => express.RequestHandler
scopes: string
options
-logger?: Logger
- onAuthorizationFailedHandler?: onAuthorizationFailedHandler - Custom handler for failed authorizations
precedenceOptions?: precedenceOptions
- Function
Toggle description
Logging is an essential part of Authmosphere's tooling. Authmosphere does not rely on console
or any other specific logger library, instead every function expects a (optional) reference to an external logger. The logger must fulfill this interface:
interface Logger {
info(message: string, error?: any): void;
debug(message: string, error?: any): void;
error(message: string, error?: any): void;
fatal(message: string, error?: any): void;
trace(message: string, error?: any): void;
warn(message: string, error?: any): void;
}
Toggle description
type OAuthConfig =
ClientCredentialsGrantConfig |
AuthorizationCodeGrantConfig |
PasswordCredentialsGrantConfig |
RefreshGrantConfig;
Toggle description
type ClientCredentialsGrantConfig = {
grantType: string;
accessTokenEndpoint: string;
queryParams?: { [index: string]: string };
bodyParams?: { [index: string]: string };
scopes?: string[];
credentialsDir: string;
}
Toggle description
type AuthorizationCodeGrantConfig = {
grantType: string;
accessTokenEndpoint: string;
queryParams?: { [index: string]: string };
bodyParams?: { [index: string]: string };
scopes?: string[];
credentialsDir: string;
code: string;
redirectUri: string;
}
Toggle description
type PasswordCredentialsGrantConfig = {
grantType: string;
accessTokenEndpoint: string;
queryParams?: { [index: string]: string };
bodyParams?: { [index: string]: string };
scopes?: string[];
credentialsDir: string;
}
Toggle description
type RefreshGrantConfig = {
grantType: string;
accessTokenEndpoint: string;
queryParams?: { [index: string]: string };
bodyParams?: { [index: string]: string };
scopes?: string[];
credentialsDir: string;
refreshToken: string;
}
Toggle description
Instead of providing a credentials directory (credentialsDir
) client and user credentials can be passed explicitly.
type ClientCredentialsGrantConfig = {
grantType: string;
accessTokenEndpoint: string;
queryParams?: { [index: string]: string };
bodyParams?: { [index: string]: string };
scopes?: string[];
clientId: string,
clientSecret: string
}
Client credentials can be passed in via clientId
and clientSecret
, user credentials via applicationUsername
and applicationPassword
;
Toggle description
type Token<CustomTokenPart = {}> = CustomTokenPart & {
access_token: string;
expires_in?: number;
scope?: string[];
token_type?: string;
local_expiry?: number;
};
Token type it can be extend to satisfy special needs:
const mytoken: Token<{ id: number }> = {
access_token: 'abcToken',
id: 2424242828
}
This tooling provides an abstraction to easily mock OAuth 2.0 RFC 6749 related endpoints.
It helps to easily write integration tests without writing extensive boilerplate code and without disabling OAuth for tests.
This tooling is based on Nock, a HTTP mocking library. For more information about Nock see Nock documentation.
Toggle description
Creates a very basic mock of token endpoint as defined in RFC 6749.
The mocked endpoint will return a token with the scopes specified in the request.
⚠️ The mock does not validate the request⚠️ The mock holds a state that contains the created tokens⚠️ cleanMock
resets the state and removes all nocks
mockAccessTokenEndpoint({
url: 'https://example.com/access_token',
times: 1
});
mockAccessTokenEndpoint(options) => nock.Scope
options
:url: string
- URL of the Token validation endpointtimes?: number
- Defines number of calls the endpoint is mocked, default isNumber.MAX_SAFE_INTEGER
Toggle description
Creates a very basic mock of a token validation endpoint.
Returns 200
and a Token object if the given token is valid (via query parameter).
The token is valid:
- If it is specified via options parameter
- If it was created by
mockAccessTokenEndpoint
Return 400
if the given Token is invalid.
The optional tokens
property in MockOptions
can be used to restrict the list of valid access_tokens.
mockTokeninfoEndpoint(
{
url: 'https://example.com//token_validation',
times: 1
},
tokens: [
{
access_token: 'someToken123',
scope: ['uid', 'something.read', 'something.write']
}
]
);
mockTokeninfoEndpoint(options, tokens) => nock.Scope
options
:url: string
- URL of the Token validation endpointtimes?: number
- Defines number of calls the endpoint is mocked, default isNumber.MAX_SAFE_INTEGER
tokens?: Token[]
- List of valid tokens and their scopes.
Toggle description
- Remove all
nock
mocks (not only from this lib, really ALL) - Resets the token state object used by
mockTokeninfoEndpoint
andmockAccessTokenEndpoint
and given tokens.
Hint:
Helpful when having multiple tests in a test suite, you can call cleanMock()
in the afterEach()
callback for example.
cleanMock();
Toggle description
To mock failing OAuth Endpoints use this mocks:
mockAccessTokenEndpointWithErrorResponse
mockTokeninfoEndpointWithErrorResponse
mockAccessTokenEndpointWithErrorResponse({
url: 'https://example.com/access_token',
times: 1
}, 401, { status: 'foo' });
mockAccessTokenEndpointWithErrorResponse(options, httpStatus, responseBody) => nock.Scope
options
:url: string
- URL of the Token validation endpointtimes?: number
- Defines number of calls the endpoint is mocked, default isNumber.MAX_SAFE_INTEGER
httpStatus: number
- StatusCode of the responseresponseBody?: object
- Body of the response