Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add assertion framework support to client authentication #336

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 9,
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures" : {
"globalReturn": false,
Expand Down
16 changes: 16 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ declare namespace OAuth2Server {
extendedGrantTypes?: Record<string, typeof AbstractGrantType>;
}

interface AssertionCredential {
clientAssertion: string;
clientAssertionType: string;
clientId?: string;
}

/**
* For returning falsey parameters in cases of failure
*/
Expand All @@ -258,6 +264,16 @@ declare namespace OAuth2Server {
*
*/
saveToken(token: Token, client: Client, user: User): Promise<Token | Falsey>;

/**
* Invoked to retrieve a client using a client assertion.
*
* It is for the model to decide if it supports the assertion framework and, if so, which
* assertion frameworks are supported. The function can return null if no model is found or
* throw an `InvalidClientError` if the assertion is invalid or not supported.
*
*/
getClientFromAssertion?(assertion: AssertionCredential): Promise<Client | Falsey>;
}

interface RequestAuthenticationModel {
Expand Down
48 changes: 35 additions & 13 deletions lib/handlers/token-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,25 +114,36 @@ class TokenHandler {
const grantType = request.body.grant_type;
const codeVerifier = request.body.code_verifier;
const isPkce = pkce.isPKCERequest({ grantType, codeVerifier });
const isAssertion = this.isClientAssertionRequest(request);

if (!credentials.clientId) {
throw new InvalidRequestError('Missing parameter: `client_id`');
}
// @todo - if multiple authentication schemes exist, throw an error
if (!isAssertion) {
if (!credentials.clientId) {
throw new InvalidRequestError('Missing parameter: `client_id`');
}

if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) {
throw new InvalidRequestError('Missing parameter: `client_secret`');
}
if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) {
throw new InvalidRequestError('Missing parameter: `client_secret`');
}

if (!isFormat.vschar(credentials.clientId)) {
throw new InvalidRequestError('Invalid parameter: `client_id`');
}
if (!isFormat.vschar(credentials.clientId)) {
throw new InvalidRequestError('Invalid parameter: `client_id`');
}

if (credentials.clientSecret && !isFormat.vschar(credentials.clientSecret)) {
throw new InvalidRequestError('Invalid parameter: `client_secret`');
if (credentials.clientSecret && !isFormat.vschar(credentials.clientSecret)) {
throw new InvalidRequestError('Invalid parameter: `client_secret`');
}
} else {
if (!credentials.clientAssertion) {
throw new InvalidClientError('Missing parameter: `client_assertion`');
}
if (!credentials.clientAssertionType) {
throw new InvalidClientError('Missing parameter: `client_assertion_type`');
}
}

try {
const client = await this.model.getClient(credentials.clientId, credentials.clientSecret);
const client = await (isAssertion ? this.model.getClientFromAssertion?.(credentials) : this.model.getClient(credentials.clientId, credentials.clientSecret));

if (!client) {
throw new InvalidClientError('Invalid client: client is invalid');
Expand Down Expand Up @@ -167,7 +178,10 @@ class TokenHandler {
* The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively,
* the `client_id` and `client_secret` can be embedded in the body.
*
* @see https://tools.ietf.org/html/rfc6749#section-2.3.1
* Also support the assertion framework for client authentication.
*
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
* @see https://datatracker.ietf.org/doc/html/rfc7521
*/

getClientCredentials (request) {
Expand All @@ -183,6 +197,10 @@ class TokenHandler {
return { clientId: request.body.client_id, clientSecret: request.body.client_secret };
}

if (this.isClientAssertionRequest(request)) {
return { clientId: request.body.client_id, clientAssertion: request.body.client_assertion, clientAssertionType: request.body.client_assertion_type };
}

if (pkce.isPKCERequest({ grantType, codeVerifier })) {
if(request.body.client_id) {
return { clientId: request.body.client_id };
Expand Down Expand Up @@ -289,6 +307,10 @@ class TokenHandler {
response.status = error.code;
}

isClientAssertionRequest({ body }) {
return body.client_assertion && body.client_assertion_type;
}

/**
* Given a grant type, check if client authentication is required
*/
Expand Down
Loading