OAuth2 implementation for Whook servers
This module is aimed to allow you to easily bring OAuth2 to your Whook server. It must be used with a server side rendered frontend that brings the UI that allows users to authenticate and allow client applications to act on behalf of them.
The module provides:
- 2 OAuth2 handlers implementing the 2 OAuth2 standard endpoints
(
getOAuth2authorize
,postOAuth2Tokentoken
) to be used by OAuth2 client applications, - 4 authentication endpoints to be used by the authorization server directly to
authenticate users (
postAuthLogin
,postAuthRefresh
,postAuthLogout
) and acknowledge the client requests (postOAuth2Acknowledge
), - 5 OAuth2 services implementing the standard grant types. You can create your own granter services to create additional grant types.
This module requires you to implement some services it relies on:
-
oAuth2AccessToken
that generates and checks theaccess_token
and theoAuth2RefreshToken
for therefresh_token
, both have the same interface, -
checkApplication
service that is supposed to check whether an application can be used or not for a given grant type, a given scope and redirect URI, -
oAuth2PasswordService
aimed to check thepassword
grant type with your own logic (if you use it), -
oAuth2ClientCredentialsService
aimed to check theclient_credential
grant type with your own logic (if you use it), -
OAuth2CodeService
aimed to check thecode
grant type with your own logic (if you use it).
Install the module in your project:
npm i @whook/oauth2
Declare the plugin into your src/index.ts
file:
// (...)
// Setup your own whook plugins or avoid whook defaults by leaving it empty
$.register(
constant('WHOOK_PLUGINS', [
...WHOOK_DEFAULT_PLUGINS,
+ '@whook/oauth2',
'@whook/cors',
]),
);
- $.register(constant('WHOOK_PLUGINS', ['@whook/whook']));
// (...)
Declare this module types in your src/whook.d.ts
type definitions:
+import type {
+ OAuth2Config,
+} from '@whook/oauth2';
// ...
declare module 'application-services' {
// (...)
export interface AppConfig
- extends WhookBaseConfigs {}
+ extends WhookBaseConfigs,
+ AuthCookiesConfig,
+ OAuth2Config {}
// ...
}
Add the OAuth2 configuration to your config files:
// ...
+ import {
+ OAUTH2_ERRORS_DESCRIPTORS,
+ OAuth2Config,
+ } from '@whook/oauth2';
import type { AppConfig } from 'application-services';
// ...
const CONFIG: AppConfig = {
// ...
+ OAUTH2: {
+ // The SSR frontend
+ authenticateURL: 'https://auth.example.com',
+ }
- ERRORS_DESCRIPTORS: DEFAULT_ERRORS_DESCRIPTORS,
+ ERRORS_DESCRIPTORS: {
+ ...DEFAULT_ERRORS_DESCRIPTORS,
+ ...OAUTH2_ERRORS_DESCRIPTORS,
+ },
// ...
};
export default CONFIG;
The oAuth2Granters
service gather the various granters services you can use in
your application but you can write your own that uses a subset or a superset of
these granters.
Here, for example a handler that implement a verify token mechanism in order to validate a user subscription:
import { autoService } from 'knifecycle';
import { noop } from '@whook/whook';
import { YError } from 'yerror';
import type { LogService } from 'common-services';
import type { AuthenticationData } from './authentication.js';
import type {
OAuth2GranterService,
CheckApplicationService,
} from '@whook/oauth2';
import type { JWTService } from 'jwt-service';
import type { PGService } from 'postgresql-service';
export type OAuth2VerifyTokenGranterDependencies = {
checkApplication: CheckApplicationService;
jwtToken: JWTService<AuthenticationData>;
pg: Pick<PGService, 'query'>;
log?: LogService;
};
export type OAuth2VerifyTokenGranterParameters = {
verifyToken: string;
};
export type OAuth2VerifyTokenGranterService = OAuth2GranterService<
unknown,
unknown,
OAuth2VerifyTokenGranterParameters,
AuthenticationData
>;
export default autoService(initOAuth2VerifyTokenGranter);
const USER_VERIFY_QUERY = `
UPDATE users
SET roles = ARRAY['user'::role]
WHERE id = $$userId
`;
async function initOAuth2VerifyTokenGranter({
checkApplication,
jwtToken,
pg,
log = noop,
}: OAuth2VerifyTokenGranterDependencies): Promise<OAuth2VerifyTokenGranterService> {
const authenticateWithVerifyToken: OAuth2VerifyTokenGranterService['authenticator']['authenticate'] =
async ({ verifyToken }, authenticationData) => {
try {
// The client must be authenticated
if (!authenticationData) {
throw new YError('E_UNAUTHORIZED');
}
const newAuthenticationData = await jwtToken.verify(verifyToken);
await checkApplication({
applicationId: authenticationData.applicationId,
type: 'verify',
scope: newAuthenticationData.scope,
});
const result = await pg.query(USER_VERIFY_QUERY, {
userId: newAuthenticationData.userId,
});
if (result.rowCount === 0) {
throw new YError('E_ALREADY_VERIFIED', authenticationData.userId);
}
return newAuthenticationData;
} catch (err) {
if (err.code === 'E_BAD_TOKEN') {
throw YError.wrap(err as Error, 'E_BAD_REFRESH_TOKEN');
}
throw err;
}
};
log('debug', '👫 - OAuth2VerifyTokenGranter Service Initialized!');
return {
type: 'verify',
authenticator: {
grantType: 'verify_token',
authenticate: authenticateWithVerifyToken,
},
};
}
For internal use, you may prefer use cookies based auth handlers like
postLogin
, postLogout
and postRefresh
.
To do so, configure the ROOT_AUTHENTICATION_DATA
and COOKIES
configurations:
// src/production/config.ts
+ COOKIES: {
+ domain: 'example.org',
+ },
+ ROOT_AUTHENTICATION_DATA: {
+ applicationId: 'abbacaca-abba-caca-caca-abbacacacaca',
+ scope: 'user,admin',
+ },
Than import the postLogin
, postLogout
and postRefresh
handlers like so:
// src/handlers/postRefresh.ts
import {
initPostAuthRefresh,
postAuthRefreshDefinition,
authCookieHeaderParameter,
} from '@whook/oauth2';
import type { WhookAPIHandlerDefinition } from '@whook/whook';
export { authCookieHeaderParameter };
export const definition: WhookAPIHandlerDefinition = {
...postAuthRefreshDefinition,
operation: {
...postAuthRefreshDefinition.operation,
'x-whook': {
disabled: false,
},
},
};
export default initPostAuthRefresh;
Additionnaly, you could create any handler in the /auth
path in order to
receive the auth cookies. For example, you may want to serve user profiles
there.
The endpoints definitions are designed to support the standard OAuth2 definitions but can be easily overriden.
You will also have to protect the postOAuth2Acknowledge
with your own security
mechanism:
import {
initPostOAuth2Acknowledge,
postOAuth2AcknowledgeDefinition,
} from '@whook/oauth2';
import type { OpenAPIV3 } from 'openapi-types';
import type { WhookAPIHandlerDefinition } from '@whook/whook';
export default initPostOAuth2Acknowledge;
export const definition: WhookAPIHandlerDefinition = {
...postOAuth2AcknowledgeDefinition,
operation: {
...postOAuth2AcknowledgeDefinition.operation,
// Complete the definition to protect the endpoint
security: [
{
bearerAuth: ['user'],
},
],
// Optionally you can rewrite the endpoint definition
// to add more custom parameters to your endpoint
requestBody: {
required: true,
content: {
'application/json': {
schema: {
...((
postOAuth2AcknowledgeDefinition.operation
.requestBody as OpenAPIV3.RequestBodyObject
).content['application/json']
.schema as OpenAPIV3.NonArraySchemaObject),
required: [
'userId',
...(
(
postOAuth2AcknowledgeDefinition.operation
.requestBody as OpenAPIV3.RequestBodyObject
).content['application/json']
.schema as OpenAPIV3.NonArraySchemaObject
).required,
],
properties: {
...(
(
postOAuth2AcknowledgeDefinition.operation
.requestBody as OpenAPIV3.RequestBodyObject
).content['application/json']
.schema as OpenAPIV3.NonArraySchemaObject
).properties,
userId: {
type: 'string',
},
},
},
},
},
},
},
};
You will probably need to also protect the postOAuth2Token
endpoint with your
own security mecanism:
// In a `src/handlers/postOAuth2Token.ts` fileimport {
initPostOAuth2Token,
postOAuth2TokenDefinition,
postOAuth2TokenAuthorizationCodeTokenRequestBodySchema,
postOAuth2TokenPasswordTokenRequestBodySchema,
postOAuth2TokenClientCredentialsTokenRequestBodySchema,
postOAuth2TokenRefreshTokenRequestBodySchema,
postOAuth2TokenTokenBodySchema,
} from '@whook/oauth2';
import type { WhookAPIHandlerDefinition } from '@whook/whook';
export default initPostOAuth2Token;
export const definition: WhookAPIHandlerDefinition = {
...postOAuth2TokenDefinition,
operation: {
...postOAuth2TokenDefinition.operation,
security: [
{
basicAuth: ['admin'],
},
],
},
};
export {
postOAuth2TokenAuthorizationCodeTokenRequestBodySchema,
postOAuth2TokenPasswordTokenRequestBodySchema,
postOAuth2TokenClientCredentialsTokenRequestBodySchema,
postOAuth2TokenRefreshTokenRequestBodySchema,
postOAuth2TokenTokenBodySchema,
};
Or you may want to reduce the OAuth2 grant types supported:
// In a `src/handlers/getOAuth2Authorize.ts` file
import {
initGetOAuth2Authorize,
getOAuth2AuthorizeDefinition as definition,
getOAuth2AuthorizeResponseTypeParameter as baseResponseTypeParameter,
getOAuth2AuthorizeClientIdParameter as clientIdParameter,
getOAuth2AuthorizeRedirectURIParameter as redirectURIParameter,
getOAuth2AuthorizeScopeParameter as scopeParameter,
getOAuth2AuthorizeStateParameter as stateParameter,
} from '@whook/oauth2';
export default initGetOAuth2Authorize;
export {
definition,
clientIdParameter,
redirectURIParameter,
scopeParameter,
stateParameter,
};
export const responseTypeParameter = {
...baseResponseTypeParameter,
parameter: {
...baseResponseTypeParameter.parameter,
schema: {
...baseResponseTypeParameter.parameter.schema,
enum: ['code'], // Allow only the 'code' grant type
},
},
};