Vendored
+4
-2
@@ -37,8 +37,6 @@ export interface Config {
|
||||
* Configures methods of external access, ie ways for callers outside of
|
||||
* the Backstage ecosystem to get authorized for access to APIs that do
|
||||
* not permit unauthorized access.
|
||||
*
|
||||
* @deepVisibility secret
|
||||
*/
|
||||
externalAccess: Array<
|
||||
| {
|
||||
@@ -79,6 +77,8 @@ export interface Config {
|
||||
* ```sh
|
||||
* node -p 'require("crypto").randomBytes(24).toString("base64")'
|
||||
* ```
|
||||
*
|
||||
* @visibility secret
|
||||
*/
|
||||
secret: string;
|
||||
|
||||
@@ -119,6 +119,8 @@ export interface Config {
|
||||
* adding a `freben-local-dev-` prefix for debugging purposes to a
|
||||
* token that you know will be handed out for use as a personal
|
||||
* access token during development.
|
||||
*
|
||||
* @visibility secret
|
||||
*/
|
||||
token: string;
|
||||
|
||||
|
||||
@@ -82,10 +82,7 @@ export class DefaultAuthService implements AuthService {
|
||||
|
||||
const externalResult = await this.externalTokenHandler.verifyToken(token);
|
||||
if (externalResult) {
|
||||
return createCredentialsWithServicePrincipal(
|
||||
externalResult.subject,
|
||||
externalResult.token,
|
||||
);
|
||||
return createCredentialsWithServicePrincipal(externalResult.subject);
|
||||
}
|
||||
|
||||
throw new AuthenticationError('Illegal token');
|
||||
|
||||
Vendored
+1
-3
@@ -77,9 +77,7 @@ export class ExternalTokenHandler {
|
||||
|
||||
constructor(private readonly handlers: TokenHandler[]) {}
|
||||
|
||||
async verifyToken(
|
||||
token: string,
|
||||
): Promise<{ subject: string; token?: string } | undefined> {
|
||||
async verifyToken(token: string): Promise<{ subject: string } | undefined> {
|
||||
for (const handler of this.handlers) {
|
||||
const result = await handler.verifyToken(token);
|
||||
if (result) {
|
||||
|
||||
-3
@@ -54,7 +54,6 @@ describe('LegacyTokenHandler', () => {
|
||||
|
||||
await expect(tokenHandler.verifyToken(token1)).resolves.toEqual({
|
||||
subject: 'key1',
|
||||
token: token1,
|
||||
});
|
||||
|
||||
const token2 = await new SignJWT({
|
||||
@@ -66,7 +65,6 @@ describe('LegacyTokenHandler', () => {
|
||||
|
||||
await expect(tokenHandler.verifyToken(token2)).resolves.toEqual({
|
||||
subject: 'key2',
|
||||
token: token2,
|
||||
});
|
||||
|
||||
const token3 = await new SignJWT({
|
||||
@@ -78,7 +76,6 @@ describe('LegacyTokenHandler', () => {
|
||||
|
||||
await expect(tokenHandler.verifyToken(token3)).resolves.toEqual({
|
||||
subject: 'external:backstage-plugin',
|
||||
token: token3,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-7
@@ -55,9 +55,7 @@ export class LegacyTokenHandler implements TokenHandler {
|
||||
this.#entries.push({ key, subject });
|
||||
}
|
||||
|
||||
async verifyToken(
|
||||
token: string,
|
||||
): Promise<{ subject: string; token?: string } | undefined> {
|
||||
async verifyToken(token: string) {
|
||||
// First do a duck typing check to see if it remotely looks like a legacy token
|
||||
try {
|
||||
// We do a fair amount of checking upfront here. Since we aren't certain
|
||||
@@ -81,10 +79,7 @@ export class LegacyTokenHandler implements TokenHandler {
|
||||
for (const entry of this.#entries) {
|
||||
try {
|
||||
await jwtVerify(token, entry.key);
|
||||
return {
|
||||
subject: entry.subject,
|
||||
token: token,
|
||||
};
|
||||
return { subject: entry.subject };
|
||||
} catch (e) {
|
||||
if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
|
||||
throw e;
|
||||
|
||||
+57
-23
@@ -20,18 +20,16 @@ import { StaticTokenHandler } from './static';
|
||||
describe('StaticTokenHandler', () => {
|
||||
it('accepts any of the added list of tokens', async () => {
|
||||
const handler = new StaticTokenHandler();
|
||||
handler.add(new ConfigReader({ token: 'abc', subject: 'one' }));
|
||||
handler.add(new ConfigReader({ token: 'def', subject: 'two' }));
|
||||
handler.add(new ConfigReader({ token: 'abcabcabc', subject: 'one' }));
|
||||
handler.add(new ConfigReader({ token: 'defdefdef', subject: 'two' }));
|
||||
|
||||
await expect(handler.verifyToken('abc')).resolves.toEqual({
|
||||
await expect(handler.verifyToken('abcabcabc')).resolves.toEqual({
|
||||
subject: 'one',
|
||||
token: 'abc',
|
||||
});
|
||||
await expect(handler.verifyToken('def')).resolves.toEqual({
|
||||
await expect(handler.verifyToken('defdefdef')).resolves.toEqual({
|
||||
subject: 'two',
|
||||
token: 'def',
|
||||
});
|
||||
await expect(handler.verifyToken('ghi')).resolves.toBeUndefined();
|
||||
await expect(handler.verifyToken('ghighighi')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('gracefully handles no added tokens', async () => {
|
||||
@@ -44,34 +42,70 @@ describe('StaticTokenHandler', () => {
|
||||
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ _missingtoken: true, subject: 'ok' })),
|
||||
).toThrow(/token/);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Missing required config value at 'token' in 'mock-config'"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: '', subject: 'ok' })),
|
||||
).toThrow(/token/);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid type in config for key 'token' in 'mock-config', got empty-string, wanted string"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'has spaces', subject: 'ok' })),
|
||||
).toThrow(/token/);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Illegal token, must be a set of non-space characters"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'hasnewline\n', subject: 'ok' })),
|
||||
).toThrow(/token/);
|
||||
handler.add(
|
||||
new ConfigReader({
|
||||
token: 'hasnewlinebutislongenough\n',
|
||||
subject: 'ok',
|
||||
}),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Illegal token, must be a set of non-space characters"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'short', subject: 'ok' })),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Illegal token, must be at least 8 characters length"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 3, subject: 'ok' })),
|
||||
).toThrow(/token/);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid type in config for key 'token' in 'mock-config', got number, wanted string"`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'ok', _missingsubject: true })),
|
||||
).toThrow(/subject/);
|
||||
handler.add(
|
||||
new ConfigReader({ token: 'validtoken', _missingsubject: true }),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Missing required config value at 'subject' in 'mock-config'"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'ok', subject: '' })),
|
||||
).toThrow(/subject/);
|
||||
handler.add(new ConfigReader({ token: 'validtoken', subject: '' })),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid type in config for key 'subject' in 'mock-config', got empty-string, wanted string"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'ok', subject: 'has spaces' })),
|
||||
).toThrow(/subject/);
|
||||
handler.add(
|
||||
new ConfigReader({ token: 'validtoken', subject: 'has spaces' }),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Illegal subject, must be a set of non-space characters"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'ok', subject: 'hasnewline\n' })),
|
||||
).toThrow(/subject/);
|
||||
handler.add(
|
||||
new ConfigReader({ token: 'validtoken', subject: 'hasnewline\n' }),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Illegal subject, must be a set of non-space characters"`,
|
||||
);
|
||||
expect(() =>
|
||||
handler.add(new ConfigReader({ token: 'ok', subject: 3 })),
|
||||
).toThrow(/subject/);
|
||||
handler.add(new ConfigReader({ token: 'validtoken', subject: 3 })),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+9
-7
@@ -17,6 +17,8 @@
|
||||
import { Config } from '@backstage/config';
|
||||
import { TokenHandler } from './types';
|
||||
|
||||
const MIN_TOKEN_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* Handles `type: static` access.
|
||||
*
|
||||
@@ -30,6 +32,11 @@ export class StaticTokenHandler implements TokenHandler {
|
||||
if (!token.match(/^\S+$/)) {
|
||||
throw new Error('Illegal token, must be a set of non-space characters');
|
||||
}
|
||||
if (token.length < MIN_TOKEN_LENGTH) {
|
||||
throw new Error(
|
||||
`Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`,
|
||||
);
|
||||
}
|
||||
|
||||
const subject = options.getString('subject');
|
||||
if (!subject.match(/^\S+$/)) {
|
||||
@@ -39,17 +46,12 @@ export class StaticTokenHandler implements TokenHandler {
|
||||
this.#entries.push({ token, subject });
|
||||
}
|
||||
|
||||
async verifyToken(
|
||||
token: string,
|
||||
): Promise<{ subject: string; token: string } | undefined> {
|
||||
async verifyToken(token: string) {
|
||||
const entry = this.#entries.find(e => e.token === token);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
subject: entry.subject,
|
||||
token: token,
|
||||
};
|
||||
return { subject: entry.subject };
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -18,7 +18,5 @@ import { Config } from '@backstage/config';
|
||||
|
||||
export interface TokenHandler {
|
||||
add(options: Config): void;
|
||||
verifyToken(
|
||||
token: string,
|
||||
): Promise<{ subject: string; token?: string } | undefined>;
|
||||
verifyToken(token: string): Promise<{ subject: string } | undefined>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user