diff --git a/src/client.ts b/src/client.ts index b035e65..e7f29c5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5,6 +5,7 @@ import { IntrospectionRequest, IntrospectionResponse, PasswordRequest, + JwtBearerRequest, OAuth2TokenTypeHint, RefreshRequest, RevocationRequest, @@ -42,6 +43,15 @@ type PasswordParams = { } +type JwtBearerParams = { + /** + * The JSON Web Token to use for the JWT Bearer token request. + */ + assertion: string; + + scope?: string[]; +} + /** * Extra options that may be passed to refresh() */ @@ -79,8 +89,8 @@ export interface ClientSettings { * OAuth2 clientSecret * * This is required when using the 'client_secret_basic' authenticationMethod - * for the client_credentials and password flows, but not authorization_code - * or implicit. + * for the client_credentials and password flows, but not authorization_code, + * implicit or JWT Bearer. */ clientSecret?: string; @@ -225,6 +235,19 @@ export class OAuth2Client { } + /** + * Retrieves an OAuth2 token using the 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant. + */ + async jwtBearer(params: JwtBearerParams): Promise { + + const body: JwtBearerRequest = { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: params.assertion, + scope: params.scope?.join(' '), + }; + return this.tokenResponseToOAuth2Token(this.request('tokenEndpoint', body)); + } + /** * Returns the helper object for the `authorization_code` grant. */ @@ -366,7 +389,7 @@ export class OAuth2Client { /** * Does a HTTP request on the 'token' endpoint. */ - async request(endpoint: 'tokenEndpoint', body: RefreshRequest | ClientCredentialsRequest | PasswordRequest | AuthorizationCodeRequest): Promise; + async request(endpoint: 'tokenEndpoint', body: RefreshRequest | ClientCredentialsRequest | PasswordRequest | JwtBearerRequest | AuthorizationCodeRequest): Promise; async request(endpoint: 'introspectionEndpoint', body: IntrospectionRequest): Promise; async request(endpoint: 'revocationEndpoint', body: RevocationRequest): Promise; async request(endpoint: OAuth2Endpoint, body: Record): Promise { diff --git a/src/messages.ts b/src/messages.ts index f05e955..5f44bf1 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -50,6 +50,15 @@ export type PasswordRequest = { resource?: string | string[]; } +/** + * JWT Bearer Grant Type request body + */ +export type JwtBearerRequest = { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + assertion: string; + scope?: string; +} + export type AuthorizationCodeRequest = { grant_type: 'authorization_code'; code: string; diff --git a/test/jwt-bearer.ts b/test/jwt-bearer.ts new file mode 100644 index 0000000..9c43c5c --- /dev/null +++ b/test/jwt-bearer.ts @@ -0,0 +1,39 @@ +import { testServer } from './test-server'; +import { OAuth2Client } from '../src'; +import { expect } from 'chai'; + +describe('jwt-bearer', () => { + + it('should work with client_secret_post', async () => { + + const server = testServer(); + + const client = new OAuth2Client({ + server: server.url, + tokenEndpoint: '/token', + clientId: 'test-client-id', + }); + + const result = await client.jwtBearer({ + assertion: 'foobar', + scope: ['hello', 'world'] + }); + + expect(result.accessToken).to.equal('access_token_000'); + expect(result.refreshToken).to.equal('refresh_token_000'); + expect(result.expiresAt).to.be.lessThanOrEqual(Date.now() + 3600_000); + expect(result.expiresAt).to.be.greaterThanOrEqual(Date.now() + 3500_000); + + const request = server.lastRequest(); + expect(request.headers.get('Authorization')).to.be.null; + expect(request.headers.get('Accept')).to.equal('application/json'); + + expect(request.body).to.eql({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: 'foobar', + scope: 'hello world', + client_id: 'test-client-id' + }); + }); + +});