feat: started to add some tests for the oidc database
Signed-off-by: benjdlambert <ben@blam.sh> Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -41,6 +41,11 @@ exports.up = async function up(knex) {
|
||||
.notNullable()
|
||||
.comment('The name of the client, should be human readable');
|
||||
|
||||
table
|
||||
.text('redirect_uris', 'longtext')
|
||||
.notNullable()
|
||||
.comment('JSON array of valid redirect URIs');
|
||||
|
||||
table
|
||||
.timestamp('created_at', { useTz: false, precision: 0 })
|
||||
.notNullable()
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { AuthDatabase } from './AuthDatabase';
|
||||
import { OidcDatabase } from './OidcDatabase';
|
||||
import { resolvePackagePath } from '@backstage/backend-plugin-api';
|
||||
|
||||
describe('Oidc Database', () => {
|
||||
const databases = TestDatabases.create();
|
||||
|
||||
async function createOidcDatabase(databaseId: TestDatabaseId) {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
await knex.migrate.latest({
|
||||
directory: resolvePackagePath(
|
||||
'@backstage/plugin-auth-backend',
|
||||
'migrations',
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
oidc: await OidcDatabase.create({
|
||||
database: AuthDatabase.create({
|
||||
getClient: async () => knex,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe.each(databases.eachSupportedId())('%p', databaseId => {
|
||||
describe('Client', () => {
|
||||
it('should create and return a client', async () => {
|
||||
const { oidc } = await createOidcDatabase(databaseId);
|
||||
|
||||
await expect(
|
||||
oidc.createClient({
|
||||
clientId: 'test-client',
|
||||
clientName: 'Test Client',
|
||||
clientSecret: 'test-secret',
|
||||
redirectUris: ['https://example.com/callback'],
|
||||
responseTypes: ['code'],
|
||||
grantTypes: ['authorization_code'],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
clientId: 'test-client',
|
||||
clientName: 'Test Client',
|
||||
clientSecret: 'test-secret',
|
||||
redirectUris: ['https://example.com/callback'],
|
||||
responseTypes: ['code'],
|
||||
grantTypes: ['authorization_code'],
|
||||
scope: undefined,
|
||||
expiresAt: undefined,
|
||||
metadata: undefined,
|
||||
createdAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the client thats created in a list', async () => {
|
||||
const { oidc } = await createOidcDatabase(databaseId);
|
||||
|
||||
const client = await oidc.createClient({
|
||||
clientId: 'test-client',
|
||||
clientName: 'Test Client',
|
||||
clientSecret: 'test-secret',
|
||||
redirectUris: ['https://example.com/callback'],
|
||||
responseTypes: ['code'],
|
||||
grantTypes: ['authorization_code'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
oidc.getClient({ clientId: 'test-client' }),
|
||||
).resolves.toEqual(client);
|
||||
});
|
||||
|
||||
it('should return null if the client does not exist', async () => {
|
||||
const { oidc } = await createOidcDatabase(databaseId);
|
||||
|
||||
await expect(
|
||||
oidc.getClient({ clientId: 'test-client' }),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authorization Code', () => {
|
||||
it('should create and return an authorization code', 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 authorizationCode = await oidc.createAuthorizationCode({
|
||||
code: 'test-code',
|
||||
clientId: mockClient.clientId,
|
||||
userEntityRef: 'user:default/blam',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
scope: undefined,
|
||||
codeChallenge: 'test-challenge',
|
||||
codeChallengeMethod: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
expiresAt: '2025-01-01',
|
||||
});
|
||||
|
||||
await expect(
|
||||
oidc.getAuthorizationCode({ code: 'test-code' }),
|
||||
).resolves.toEqual(authorizationCode);
|
||||
});
|
||||
|
||||
it('should return null if the authorization code does not exist', async () => {
|
||||
const { oidc } = await createOidcDatabase(databaseId);
|
||||
|
||||
await expect(
|
||||
oidc.getAuthorizationCode({ code: 'test-code' }),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('should return the authorization code 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 authorizationCode = await oidc.createAuthorizationCode({
|
||||
code: 'test-code',
|
||||
clientId: mockClient.clientId,
|
||||
userEntityRef: 'user:default/blam',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
scope: undefined,
|
||||
codeChallenge: 'test-challenge',
|
||||
codeChallengeMethod: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
expiresAt: '2025-01-01',
|
||||
});
|
||||
|
||||
await expect(
|
||||
oidc.getAuthorizationCode({ code: 'test-code' }),
|
||||
).resolves.toEqual(authorizationCode);
|
||||
});
|
||||
|
||||
it('should allow updating the authorization code', 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 authorizationCode = await oidc.createAuthorizationCode({
|
||||
code: 'test-code',
|
||||
clientId: mockClient.clientId,
|
||||
userEntityRef: 'user:default/blam',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
codeChallenge: 'test-challenge',
|
||||
codeChallengeMethod: 'S256',
|
||||
nonce: 'test-nonce',
|
||||
expiresAt: '2025-01-01',
|
||||
});
|
||||
|
||||
await expect(
|
||||
oidc.updateAuthorizationCode({
|
||||
code: 'test-code',
|
||||
used: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
...authorizationCode,
|
||||
used: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Knex } from 'knex';
|
||||
import { AuthDatabase } from './AuthDatabase';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
type OidcClientRow = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
client_name: string;
|
||||
created_at: string;
|
||||
expires_at: string | null;
|
||||
response_types: string;
|
||||
grant_types: string;
|
||||
redirect_uris: string;
|
||||
scope: string | null;
|
||||
metadata: string | null;
|
||||
};
|
||||
|
||||
type OidcAuthorizationCodeRow = {
|
||||
code: string;
|
||||
client_id: string;
|
||||
user_entity_ref: string;
|
||||
redirect_uri: string;
|
||||
scope: string | null;
|
||||
code_challenge: string | null;
|
||||
code_challenge_method: string | null;
|
||||
nonce: string | null;
|
||||
created_at: string;
|
||||
expires_at: string;
|
||||
used?: boolean;
|
||||
};
|
||||
|
||||
type Client = {
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
clientSecret: string;
|
||||
redirectUris: string[];
|
||||
responseTypes: string[];
|
||||
grantTypes: string[];
|
||||
scope?: string;
|
||||
expiresAt?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type AuthorizationCode = {
|
||||
code: string;
|
||||
clientId: string;
|
||||
userEntityRef: string;
|
||||
redirectUri: string;
|
||||
scope?: string;
|
||||
codeChallenge?: string;
|
||||
codeChallengeMethod?: string;
|
||||
nonce?: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
export class OidcDatabase {
|
||||
private constructor(private readonly db: Knex) {}
|
||||
|
||||
static async create(options: { database: AuthDatabase }) {
|
||||
const client = await options.database.get();
|
||||
return new OidcDatabase(client);
|
||||
}
|
||||
|
||||
async createClient(client: Omit<Client, 'createdAt'>) {
|
||||
const now = DateTime.now().toString();
|
||||
|
||||
await this.db<OidcClientRow>('oidc_clients').insert({
|
||||
client_id: client.clientId,
|
||||
client_secret: client.clientSecret,
|
||||
client_name: client.clientName,
|
||||
created_at: now,
|
||||
expires_at: client.expiresAt,
|
||||
response_types: JSON.stringify(client.responseTypes),
|
||||
grant_types: JSON.stringify(client.grantTypes),
|
||||
redirect_uris: JSON.stringify(client.redirectUris),
|
||||
scope: client.scope,
|
||||
metadata: JSON.stringify(client.metadata),
|
||||
});
|
||||
|
||||
return {
|
||||
...client,
|
||||
createdAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
async getClient({ clientId }: { clientId: string }) {
|
||||
const client = await this.db<OidcClientRow>('oidc_clients')
|
||||
.where('client_id', clientId)
|
||||
.first();
|
||||
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.rowToClient(client) as Client;
|
||||
}
|
||||
|
||||
async createAuthorizationCode(
|
||||
authorizationCode: Omit<AuthorizationCode, 'createdAt' | 'used'>,
|
||||
) {
|
||||
const now = DateTime.now().toString();
|
||||
|
||||
await this.db<OidcAuthorizationCodeRow>('oidc_authorization_codes').insert({
|
||||
code: authorizationCode.code,
|
||||
client_id: authorizationCode.clientId,
|
||||
user_entity_ref: authorizationCode.userEntityRef,
|
||||
redirect_uri: authorizationCode.redirectUri,
|
||||
scope: authorizationCode.scope,
|
||||
code_challenge: authorizationCode.codeChallenge,
|
||||
code_challenge_method: authorizationCode.codeChallengeMethod,
|
||||
nonce: authorizationCode.nonce,
|
||||
expires_at: authorizationCode.expiresAt,
|
||||
created_at: now,
|
||||
used: false,
|
||||
});
|
||||
|
||||
return {
|
||||
...authorizationCode,
|
||||
createdAt: now,
|
||||
used: false,
|
||||
};
|
||||
}
|
||||
|
||||
async getAuthorizationCode({ code }: { code: string }) {
|
||||
const authorizationCode = await this.db<OidcAuthorizationCodeRow>(
|
||||
'oidc_authorization_codes',
|
||||
)
|
||||
.where('code', code)
|
||||
.first();
|
||||
|
||||
if (!authorizationCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.rowToAuthorizationCode(authorizationCode) as AuthorizationCode;
|
||||
}
|
||||
|
||||
async updateAuthorizationCode(
|
||||
authorizationCode: Partial<AuthorizationCode> & { code: string },
|
||||
) {
|
||||
const row = this.authorizationCodeToRow(authorizationCode);
|
||||
const updatedFields = Object.fromEntries(
|
||||
Object.entries(row).filter(([_, value]) => value !== undefined),
|
||||
);
|
||||
console.log(updatedFields);
|
||||
const updated = await this.db<OidcAuthorizationCodeRow>(
|
||||
'oidc_authorization_codes',
|
||||
)
|
||||
.where('code', authorizationCode.code)
|
||||
.update(updatedFields)
|
||||
.returning('*');
|
||||
|
||||
return this.rowToAuthorizationCode(updated[0]) as AuthorizationCode;
|
||||
}
|
||||
|
||||
private rowToClient(row: Partial<OidcClientRow>): Partial<Client> {
|
||||
return {
|
||||
clientId: row.client_id,
|
||||
clientName: row.client_name,
|
||||
clientSecret: row.client_secret,
|
||||
redirectUris: row.redirect_uris
|
||||
? JSON.parse(row.redirect_uris)
|
||||
: undefined,
|
||||
responseTypes: row.response_types
|
||||
? JSON.parse(row.response_types)
|
||||
: undefined,
|
||||
grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined,
|
||||
scope: row.scope ?? undefined,
|
||||
expiresAt: row.expires_at ?? undefined,
|
||||
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
private authorizationCodeToRow(
|
||||
authorizationCode: Partial<AuthorizationCode>,
|
||||
): Partial<OidcAuthorizationCodeRow> {
|
||||
return {
|
||||
code: authorizationCode.code,
|
||||
client_id: authorizationCode.clientId,
|
||||
user_entity_ref: authorizationCode.userEntityRef,
|
||||
redirect_uri: authorizationCode.redirectUri,
|
||||
scope: authorizationCode.scope,
|
||||
code_challenge: authorizationCode.codeChallenge,
|
||||
code_challenge_method: authorizationCode.codeChallengeMethod,
|
||||
nonce: authorizationCode.nonce,
|
||||
created_at: authorizationCode.createdAt,
|
||||
expires_at: authorizationCode.expiresAt,
|
||||
used: authorizationCode.used,
|
||||
};
|
||||
}
|
||||
|
||||
private rowToAuthorizationCode(
|
||||
row: Partial<OidcAuthorizationCodeRow>,
|
||||
): Partial<AuthorizationCode> {
|
||||
return {
|
||||
code: row.code,
|
||||
clientId: row.client_id,
|
||||
userEntityRef: row.user_entity_ref,
|
||||
redirectUri: row.redirect_uri,
|
||||
scope: row.scope ?? undefined,
|
||||
codeChallenge: row.code_challenge ?? undefined,
|
||||
codeChallengeMethod: row.code_challenge_method ?? undefined,
|
||||
nonce: row.nonce ?? undefined,
|
||||
createdAt: row.created_at,
|
||||
expiresAt: row.expires_at,
|
||||
used: Boolean(row.used),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user