Skip to content

Commit

Permalink
feat: anonymous tokens (#6)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Kolárik <martin@kolarik.sk>
  • Loading branch information
alexey-yarmosh and MartinKolarik authored Oct 11, 2024
1 parent 06b4a41 commit bfe9c47
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 8 deletions.
15 changes: 13 additions & 2 deletions seeds/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const clients = [
name: 'App One',
secrets: '[]',
redirect_urls: JSON.stringify([ 'https://example.com/one/callback' ]),
grants: JSON.stringify([ 'authorization_code', 'refresh_token' ]),
grants: JSON.stringify([ 'authorization_code', 'refresh_token', 'client_credentials' ]),
},
{
id: 'b2a50a7e-6dc5-423d-864e-173ea690992e',
Expand All @@ -25,9 +25,20 @@ export const clients = [
redirect_urls: JSON.stringify([ 'https://example.com/two/callback' ]),
grants: JSON.stringify([ 'authorization_code', 'refresh_token' ]),
},
{
id: '3de73daa-3943-421c-9847-ce6ccc8e69c2',
user_created: users[1]!.id,
name: 'Slack App',
secrets: '["OSMOYY6tV16Kc0l+BB5ml4eKXFf4JaqARFMCdudKU98="]',
redirect_urls: JSON.stringify([ 'https://example.com/three/callback' ]),
grants: JSON.stringify([ 'client_credentials' ]),
},
];

export const secrets = new Map([ [ clients[1], 'tzc2di5tmthrbxjh7vnq3v4ymicqod7eucccblyfs4ncpr7o' ] ]);
export const secrets = new Map([
[ clients[1], 'tzc2di5tmthrbxjh7vnq3v4ymicqod7eucccblyfs4ncpr7o' ],
[ clients[2], 'bgygsrjvvwjdj73dmq7bdhrn72s4opexedf4ksjvmi6gir7a' ],
]);

export async function seed (db: Knex) {
// Insert users
Expand Down
9 changes: 9 additions & 0 deletions src/oauth/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
InternalToken,
InternalTokenRow,
InternalUser,
ClientCredentialsUser,
PublicAuthorizationCodeDetails,
Token,
User,
Expand Down Expand Up @@ -279,6 +280,14 @@ export default class OAuthModel implements AuthorizationCodeModel, RefreshTokenM
return await this.sql(OAuthModel.usersTable).where({ id }).select<InternalUser>([ 'id', 'github_username' ]).first() || null;
}

async getUserFromClient (client: ClientWithCredentials): Promise<ClientCredentialsUser | null> {
if (client.secrets.length === 0) {
throw new InvalidClientError('Invalid client: client should have secret');
}

return { id: null };
}

async revokeToken (token: RefreshToken): Promise<boolean> {
return this.revokeAnyToken(token.refreshToken, 'refresh_token');
}
Expand Down
6 changes: 3 additions & 3 deletions src/oauth/route/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import config from 'config';
import { ExtendedContext } from '../../types.js';
import { OAuthRouteOptions } from '../types.js';

Expand All @@ -9,9 +10,7 @@ export const metadataGet = (options: OAuthRouteOptions) => {
token_endpoint: `${options.serverHost}/oauth/token`,
introspection_endpoint: `${options.serverHost}/oauth/token/introspect`,
revocation_endpoint: `${options.serverHost}/oauth/token/revoke`,
scopes_supported: [
'measurements',
],
scopes_supported: config.get<string[]>('auth.validScopes'),
response_types_supported: [
'code',
'token',
Expand All @@ -21,6 +20,7 @@ export const metadataGet = (options: OAuthRouteOptions) => {
],
grant_types_supported: [
'authorization_code',
'client_credentials',
'refresh_token',
],
token_endpoint_auth_methods_supported: [
Expand Down
4 changes: 4 additions & 0 deletions src/oauth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export type InternalUser = {
github_username: string;
}

export type ClientCredentialsUser = {
id: null;
}

export type OAuthRouteOptions = {
dashHost: string;
docsHost: string;
Expand Down
89 changes: 86 additions & 3 deletions test/tests/integration/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const authorizationEndpoint = '/oauth/authorize';
const user1 = users[0]!;
const client1 = clients[0]!;
const client2 = clients[1]!;
const client3 = clients[2]!;

describe('OAuth', () => {
let app: Server;
Expand Down Expand Up @@ -106,9 +107,7 @@ describe('OAuth', () => {

describe('Authorization Endpoint', () => {
it('should successfully authorize with correct parameters and user approval', async () => {
expect(clients).to.have.length.greaterThan(1);

await Bluebird.map(clients, client => getAuthorizationCode(client));
await Bluebird.map([ client1, client2 ], client => getAuthorizationCode(client));
});

it('should remember user approval and return code right away on second request', async () => {
Expand Down Expand Up @@ -469,6 +468,90 @@ describe('OAuth', () => {
});
});

describe('Client Credentials Grant', () => {
it('should successfully authorize the client and return access token', async () => {
const res = await requestAgent
.post(tokenEndpoint)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: client3.id,
client_secret: secrets.get(client3),
grant_type: 'client_credentials',
scope: 'measurements',
});

expect(res.status).to.equal(200);
expect(res.body).to.have.property('access_token');
expect(res.body).to.not.have.property('refresh_token');
expect(res.body).to.have.property('expires_in');
expect(res.body).to.have.property('token_type', 'Bearer');
expect(res.body).to.have.property('scope', 'measurements');
});

it('should fail with wrong client_secret', async () => {
const res = await requestAgent
.post(tokenEndpoint)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: client3.id,
client_secret: 'wrongSecretValue23456723456723456723456723456723',
grant_type: 'client_credentials',
scope: 'measurements',
});

expect(res.status).to.equal(400);
expect(res.body).to.have.property('error', 'invalid_client');
expect(res.body).to.have.property('error_description').that.includes('client credentials are invalid');
});

it('should fail with missing client_secret', async () => {
const res = await requestAgent
.post(tokenEndpoint)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: client3.id,
grant_type: 'client_credentials',
scope: 'measurements',
});

expect(res.status).to.equal(400);
expect(res.body).to.have.property('error', 'invalid_client');
expect(res.body).to.have.property('error_description').that.includes('cannot retrieve client credentials');
});

it('should fail if client_credentials grant type is not specified for the client', async () => {
const res = await requestAgent
.post(tokenEndpoint)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: client2.id,
client_secret: secrets.get(client2),
grant_type: 'client_credentials',
scope: 'measurements',
});

expect(res.status).to.equal(400);
expect(res.body).to.have.property('error', 'unauthorized_client');
expect(res.body).to.have.property('error_description').that.includes('`grant_type` is invalid');
});

it('should fail for client without secret', async () => {
const res = await requestAgent
.post(tokenEndpoint)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send({
client_id: client1.id,
client_secret: 'randomSecretValue2345672345672345672345672345672',
grant_type: 'client_credentials',
scope: 'measurements',
});

expect(res.status).to.equal(400);
expect(res.body).to.have.property('error', 'invalid_client');
expect(res.body).to.have.property('error_description').that.includes('client should have secret');
});
});

describe('Token Revocation', () => {
const getTokenFromDB = (token: string) => {
return sql('gp_tokens').where({ value: createHash('sha256').update(base32.decode(token.toUpperCase())).digest('base64') }).first();
Expand Down

0 comments on commit bfe9c47

Please sign in to comment.