chore: implementing access token management

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-07-02 19:30:25 +02:00
parent 64dc5463ba
commit ac54ac21d3
2 changed files with 197 additions and 5 deletions
@@ -41,7 +41,7 @@ describe('Oidc Database', () => {
}
describe.each(databases.eachSupportedId())('%p', databaseId => {
describe('Client', () => {
describe('Clients', () => {
it('should create and return a client', async () => {
const { oidc } = await createOidcDatabase(databaseId);
@@ -94,7 +94,7 @@ describe('Oidc Database', () => {
});
});
describe('Authorization Code', () => {
describe('Authorization Codes', () => {
it('should create and return an authorization code', async () => {
const { oidc } = await createOidcDatabase(databaseId);
@@ -195,5 +195,96 @@ describe('Oidc Database', () => {
});
});
});
describe('Access Tokens', () => {
it('should create and return an access token', async () => {
const { oidc } = await createOidcDatabase(databaseId);
const mockClient = await oidc.createClient({
clientId: 'test-client',
clientName: 'Test Client',
clientSecret: 'test-secret',
redirectUris: ['https://example.com/callback'],
responseTypes: ['code'],
grantTypes: ['authorization_code'],
});
const accessToken = await oidc.createAccessToken({
tokenId: 'test-token',
clientId: mockClient.clientId,
userEntityRef: 'user:default/blam',
expiresAt: '2025-01-01',
revoked: false,
});
await expect(
oidc.getAccessToken({ tokenId: 'test-token' }),
).resolves.toEqual(accessToken);
});
it('should return null if the access token does not exist', async () => {
const { oidc } = await createOidcDatabase(databaseId);
await expect(
oidc.getAccessToken({ tokenId: 'test-token' }),
).resolves.toBeNull();
});
it('should return the access token when created', async () => {
const { oidc } = await createOidcDatabase(databaseId);
const mockClient = await oidc.createClient({
clientId: 'test-client',
clientName: 'Test Client',
clientSecret: 'test-secret',
redirectUris: ['https://example.com/callback'],
responseTypes: ['code'],
grantTypes: ['authorization_code'],
});
const accessToken = await oidc.createAccessToken({
tokenId: 'test-token',
clientId: mockClient.clientId,
userEntityRef: 'user:default/blam',
expiresAt: '2025-01-01',
revoked: false,
});
await expect(
oidc.getAccessToken({ tokenId: 'test-token' }),
).resolves.toEqual(accessToken);
});
it('should allow updating the access token', async () => {
const { oidc } = await createOidcDatabase(databaseId);
const mockClient = await oidc.createClient({
clientId: 'test-client',
clientName: 'Test Client',
clientSecret: 'test-secret',
redirectUris: ['https://example.com/callback'],
responseTypes: ['code'],
grantTypes: ['authorization_code'],
});
const accessToken = await oidc.createAccessToken({
tokenId: 'test-token',
clientId: mockClient.clientId,
userEntityRef: 'user:default/blam',
expiresAt: '2025-01-01',
revoked: false,
});
await expect(
oidc.updateAccessToken({
tokenId: 'test-token',
revoked: true,
}),
).resolves.toEqual({
...accessToken,
revoked: true,
});
});
});
});
});
@@ -45,6 +45,16 @@ type OidcAuthorizationCodeRow = {
used?: boolean;
};
type OidcAccessTokenRow = {
token_id: string;
client_id: string;
user_entity_ref: string;
scope: string | null;
created_at: string;
expires_at: string;
revoked?: boolean;
};
type Client = {
clientId: string;
clientName: string;
@@ -72,6 +82,23 @@ type AuthorizationCode = {
used: boolean;
};
type AccessToken = {
tokenId: string;
clientId: string;
userEntityRef: string;
scope?: string;
createdAt: string;
expiresAt: string;
revoked?: boolean;
};
/**
* This class is an implementation for the Database operations that power the OIDC sign-in flow.
*
* This class provides database operations for OpenID Connect (OIDC) authentication flows.
* It manages OIDC clients, authorization codes, and access tokens in the database, as well as the consent requests
* for the frontend plugin to accept.
*/
export class OidcDatabase {
private constructor(private readonly db: Knex) {}
@@ -161,15 +188,61 @@ export class OidcDatabase {
const updatedFields = Object.fromEntries(
Object.entries(row).filter(([_, value]) => value !== undefined),
);
console.log(updatedFields);
const updated = await this.db<OidcAuthorizationCodeRow>(
const [updated] = await this.db<OidcAuthorizationCodeRow>(
'oidc_authorization_codes',
)
.where('code', authorizationCode.code)
.update(updatedFields)
.returning('*');
return this.rowToAuthorizationCode(updated[0]) as AuthorizationCode;
return this.rowToAuthorizationCode(updated) as AuthorizationCode;
}
async createAccessToken(accessToken: Omit<AccessToken, 'createdAt'>) {
const now = DateTime.now().toString();
await this.db<OidcAccessTokenRow>('oidc_access_tokens').insert({
token_id: accessToken.tokenId,
client_id: accessToken.clientId,
user_entity_ref: accessToken.userEntityRef,
scope: accessToken.scope,
created_at: now,
expires_at: accessToken.expiresAt,
revoked: accessToken.revoked ?? false,
});
return {
...accessToken,
createdAt: now,
};
}
async getAccessToken({ tokenId }: { tokenId: string }) {
const accessToken = await this.db<OidcAccessTokenRow>('oidc_access_tokens')
.where('token_id', tokenId)
.first();
if (!accessToken) {
return null;
}
return this.rowToAccessToken(accessToken) as AccessToken;
}
async updateAccessToken(
accessToken: Partial<AccessToken> & { tokenId: string },
) {
const row = this.accessTokenToRow(accessToken);
const updatedFields = Object.fromEntries(
Object.entries(row).filter(([_, value]) => value !== undefined),
);
const [updated] = await this.db<OidcAccessTokenRow>('oidc_access_tokens')
.where('token_id', accessToken.tokenId)
.update(updatedFields)
.returning('*');
return this.rowToAccessToken(updated) as AccessToken;
}
private rowToClient(row: Partial<OidcClientRow>): Partial<Client> {
@@ -226,4 +299,32 @@ export class OidcDatabase {
used: Boolean(row.used),
};
}
private accessTokenToRow(
accessToken: Partial<AccessToken>,
): Partial<OidcAccessTokenRow> {
return {
token_id: accessToken.tokenId,
client_id: accessToken.clientId,
user_entity_ref: accessToken.userEntityRef,
scope: accessToken.scope,
created_at: accessToken.createdAt,
expires_at: accessToken.expiresAt,
revoked: accessToken.revoked,
};
}
private rowToAccessToken(
row: Partial<OidcAccessTokenRow>,
): Partial<AccessToken> {
return {
tokenId: row.token_id,
clientId: row.client_id,
userEntityRef: row.user_entity_ref,
scope: row.scope ?? undefined,
createdAt: row.created_at,
expiresAt: row.expires_at,
revoked: Boolean(row.revoked),
};
}
}