refactor: make token types internal
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { TokenTypes } from '@backstage/plugin-auth-node';
|
||||
import { tokenTypes } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
base64url,
|
||||
createRemoteJWKSet,
|
||||
@@ -82,18 +82,19 @@ export class UserTokenHandler {
|
||||
#getTokenVerificationOptions(token: string): JWTVerifyOptions | undefined {
|
||||
const { typ } = decodeProtectedHeader(token);
|
||||
|
||||
if (typ === TokenTypes.user.typParam) {
|
||||
return {
|
||||
algorithms: this.#algorithms,
|
||||
typ: TokenTypes.user.typParam,
|
||||
};
|
||||
}
|
||||
|
||||
if (typ === TokenTypes.limitedUser.typParam) {
|
||||
if (typ === tokenTypes.user.typParam) {
|
||||
return {
|
||||
algorithms: this.#algorithms,
|
||||
requiredClaims: ['iat', 'exp', 'sub'],
|
||||
typ: TokenTypes.limitedUser.typParam,
|
||||
typ: tokenTypes.user.typParam,
|
||||
};
|
||||
}
|
||||
|
||||
if (typ === tokenTypes.limitedUser.typParam) {
|
||||
return {
|
||||
algorithms: this.#algorithms,
|
||||
requiredClaims: ['iat', 'exp', 'sub'],
|
||||
typ: tokenTypes.limitedUser.typParam,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,6 +113,8 @@ export class UserTokenHandler {
|
||||
new TextDecoder().decode(base64url.decode(payloadRaw)),
|
||||
);
|
||||
|
||||
// NOTE: The order and properties in both the header and payload must match
|
||||
// the usage in plugins/auth-backend/src/identity/TokenFactory.ts
|
||||
const limitedUserToken = [
|
||||
base64url.encode(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -24,11 +24,7 @@ import {
|
||||
} from 'jose';
|
||||
import { MemoryKeyStore } from './MemoryKeyStore';
|
||||
import { TokenFactory } from './TokenFactory';
|
||||
import {
|
||||
BackstageUserIdentityProofPayload,
|
||||
BackstageTokenPayload,
|
||||
TokenTypes,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { tokenTypes } from '@backstage/plugin-auth-node';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -69,14 +65,11 @@ describe('TokenFactory', () => {
|
||||
const { keys } = await factory.listPublicKeys();
|
||||
const keyStore = createLocalJWKSet({ keys: keys });
|
||||
|
||||
const verifyResult = await jwtVerify<BackstageTokenPayload>(
|
||||
token,
|
||||
keyStore,
|
||||
);
|
||||
const verifyResult = await jwtVerify(token, keyStore);
|
||||
expect(verifyResult.protectedHeader.typ).toBe(tokenTypes.user.typParam);
|
||||
expect(verifyResult.payload).toEqual({
|
||||
typ: TokenTypes.user.typClaim,
|
||||
iss: 'my-issuer',
|
||||
aud: TokenTypes.user.audClaim,
|
||||
aud: tokenTypes.user.audClaim,
|
||||
sub: entityRef,
|
||||
ent: [entityRef],
|
||||
'x-fancy-claim': 'my special claim',
|
||||
@@ -92,14 +85,15 @@ describe('TokenFactory', () => {
|
||||
const limitedUserToken = [
|
||||
base64url.encode(
|
||||
JSON.stringify({
|
||||
typ: tokenTypes.limitedUser.typParam,
|
||||
alg: verifyResult.protectedHeader.alg,
|
||||
kid: verifyResult.protectedHeader.kid!,
|
||||
}),
|
||||
),
|
||||
base64url.encode(
|
||||
JSON.stringify({
|
||||
typ: TokenTypes.limitedUser.typClaim,
|
||||
sub: verifyResult.payload.sub,
|
||||
ent: verifyResult.payload.ent,
|
||||
iat: verifyResult.payload.iat,
|
||||
exp: verifyResult.payload.exp,
|
||||
}),
|
||||
@@ -107,14 +101,13 @@ describe('TokenFactory', () => {
|
||||
verifyResult.payload.uip,
|
||||
].join('.');
|
||||
|
||||
const verifyProofResult =
|
||||
await jwtVerify<BackstageUserIdentityProofPayload>(
|
||||
limitedUserToken,
|
||||
keyStore,
|
||||
);
|
||||
const verifyProofResult = await jwtVerify(limitedUserToken, keyStore);
|
||||
expect(verifyProofResult.protectedHeader.typ).toBe(
|
||||
tokenTypes.limitedUser.typParam,
|
||||
);
|
||||
expect(verifyProofResult.payload).toEqual({
|
||||
typ: TokenTypes.limitedUser.typClaim,
|
||||
sub: entityRef,
|
||||
ent: [entityRef],
|
||||
iat: expect.any(Number),
|
||||
exp: expect.any(Number),
|
||||
});
|
||||
|
||||
@@ -28,17 +28,87 @@ import {
|
||||
import { DateTime } from 'luxon';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
BackstageTokenPayload,
|
||||
BackstageUserIdentityProofPayload,
|
||||
TokenParams,
|
||||
TokenTypes,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { TokenParams, tokenTypes } from '@backstage/plugin-auth-node';
|
||||
import { AnyJWK, KeyStore, TokenIssuer } from './types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
const MS_IN_S = 1000;
|
||||
const MAX_TOKEN_LENGTH = 32768; // At 64 bytes per entity ref this still leaves room for about 500 entities
|
||||
|
||||
/**
|
||||
* The payload contents of a valid Backstage JWT token
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface BackstageTokenPayload {
|
||||
/**
|
||||
* The issuer of the token, currently the discovery URL of the auth backend
|
||||
*/
|
||||
iss: string;
|
||||
|
||||
/**
|
||||
* The entity ref of the user
|
||||
*/
|
||||
sub: string;
|
||||
|
||||
/**
|
||||
* The entity refs that the user claims ownership througg
|
||||
*/
|
||||
ent: string[];
|
||||
|
||||
/**
|
||||
* A hard coded audience string
|
||||
*/
|
||||
aud: typeof tokenTypes.user.audClaim;
|
||||
|
||||
/**
|
||||
* Standard expiry in epoch seconds
|
||||
*/
|
||||
exp: number;
|
||||
|
||||
/**
|
||||
* Standard issue time in epoch seconds
|
||||
*/
|
||||
iat: number;
|
||||
|
||||
/**
|
||||
* A separate user identity proof that the auth service can convert to a limited user token
|
||||
*/
|
||||
uip: string;
|
||||
|
||||
/**
|
||||
* Any other custom claims that the adopter may have added
|
||||
*/
|
||||
[claim: string]: JsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The payload contents of a valid Backstage user identity claim token
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface BackstageUserIdentityProofPayload {
|
||||
/**
|
||||
* The entity ref of the user
|
||||
*/
|
||||
sub: string;
|
||||
|
||||
/**
|
||||
* The ownership entity refs of the user
|
||||
*/
|
||||
ent?: string[];
|
||||
|
||||
/**
|
||||
* Standard expiry in epoch seconds
|
||||
*/
|
||||
exp: number;
|
||||
|
||||
/**
|
||||
* Standard issue time in epoch seconds
|
||||
*/
|
||||
iat: number;
|
||||
}
|
||||
|
||||
type Options = {
|
||||
logger: LoggerService;
|
||||
/** Value of the issuer claim in issued tokens */
|
||||
@@ -93,7 +163,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
|
||||
const iss = this.issuer;
|
||||
const { sub, ent = [sub], ...additionalClaims } = params.claims;
|
||||
const aud = TokenTypes.user.audClaim;
|
||||
const aud = tokenTypes.user.audClaim;
|
||||
const iat = Math.floor(Date.now() / MS_IN_S);
|
||||
const exp = iat + this.keyDurationSeconds;
|
||||
|
||||
@@ -116,7 +186,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
|
||||
const uip = await this.createUserIdentityClaim({
|
||||
header: {
|
||||
typ: TokenTypes.limitedUser.typParam,
|
||||
typ: tokenTypes.limitedUser.typParam,
|
||||
alg: key.alg,
|
||||
kid: key.kid,
|
||||
},
|
||||
@@ -137,7 +207,7 @@ export class TokenFactory implements TokenIssuer {
|
||||
|
||||
const token = await new SignJWT(claims)
|
||||
.setProtectedHeader({
|
||||
typ: TokenTypes.user.typParam,
|
||||
typ: tokenTypes.user.typParam,
|
||||
alg: key.alg,
|
||||
kid: key.kid,
|
||||
})
|
||||
|
||||
@@ -109,19 +109,6 @@ export interface BackstageSignInResult {
|
||||
token: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface BackstageTokenPayload {
|
||||
[claim: string]: JsonValue;
|
||||
aud: typeof TokenTypes.user.audClaim;
|
||||
ent: string[];
|
||||
exp: number;
|
||||
iat: number;
|
||||
iss: string;
|
||||
sub: string;
|
||||
typ: typeof TokenTypes.user.typClaim;
|
||||
uip: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type BackstageUserIdentity = {
|
||||
type: 'user';
|
||||
@@ -129,14 +116,6 @@ export type BackstageUserIdentity = {
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface BackstageUserIdentityProofPayload {
|
||||
exp: number;
|
||||
iat: number;
|
||||
sub: string;
|
||||
typ: typeof TokenTypes.limitedUser.typClaim;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ClientAuthResponse<TProviderInfo> = {
|
||||
providerInfo: TProviderInfo;
|
||||
@@ -661,16 +640,16 @@ export type TokenParams = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export const TokenTypes: Readonly<{
|
||||
export const tokenTypes: Readonly<{
|
||||
user: Readonly<{
|
||||
typClaim: 'vnd.backstage.user';
|
||||
typParam: 'vnd.backstage.user';
|
||||
audClaim: 'backstage';
|
||||
}>;
|
||||
limitedUser: Readonly<{
|
||||
typClaim: 'vnd.backstage.limited-user';
|
||||
typParam: 'vnd.backstage.limited-user';
|
||||
}>;
|
||||
service: Readonly<{
|
||||
typClaim: 'vnd.backstage.service';
|
||||
typParam: 'vnd.backstage.service';
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ export type {
|
||||
AuthResolverContext,
|
||||
BackstageIdentityResponse,
|
||||
BackstageSignInResult,
|
||||
BackstageTokenPayload,
|
||||
BackstageUserIdentity,
|
||||
BackstageUserIdentityProofPayload,
|
||||
ClientAuthResponse,
|
||||
CookieConfigurer,
|
||||
ProfileInfo,
|
||||
@@ -46,4 +44,4 @@ export type {
|
||||
SignInResolver,
|
||||
TokenParams,
|
||||
} from './types';
|
||||
export { TokenTypes } from './types';
|
||||
export { tokenTypes } from './types';
|
||||
|
||||
@@ -381,7 +381,7 @@ export type CookieConfigurer = (ctx: {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TokenTypes = Object.freeze({
|
||||
export const tokenTypes = Object.freeze({
|
||||
user: Object.freeze({
|
||||
typParam: 'vnd.backstage.user',
|
||||
audClaim: 'backstage',
|
||||
@@ -393,77 +393,3 @@ export const TokenTypes = Object.freeze({
|
||||
typParam: 'vnd.backstage.service',
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The payload contents of a valid Backstage JWT token
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BackstageTokenPayload {
|
||||
/**
|
||||
* The issuer of the token, currently the discovery URL of the auth backend
|
||||
*/
|
||||
iss: string;
|
||||
|
||||
/**
|
||||
* The entity ref of the user
|
||||
*/
|
||||
sub: string;
|
||||
|
||||
/**
|
||||
* The entity refs that the user claims ownership througg
|
||||
*/
|
||||
ent: string[];
|
||||
|
||||
/**
|
||||
* A hard coded audience string
|
||||
*/
|
||||
aud: typeof TokenTypes.user.audClaim;
|
||||
|
||||
/**
|
||||
* Standard expiry in epoch seconds
|
||||
*/
|
||||
exp: number;
|
||||
|
||||
/**
|
||||
* Standard issue time in epoch seconds
|
||||
*/
|
||||
iat: number;
|
||||
|
||||
/**
|
||||
* A separate user identity proof that the auth service can convert to a limited user token
|
||||
*/
|
||||
uip: string;
|
||||
|
||||
/**
|
||||
* Any other custom claims that the adopter may have added
|
||||
*/
|
||||
[claim: string]: JsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The payload contents of a valid Backstage user identity claim token
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BackstageUserIdentityProofPayload {
|
||||
/**
|
||||
* The entity ref of the user
|
||||
*/
|
||||
sub: string;
|
||||
|
||||
/**
|
||||
* The ownership entity refs of the user
|
||||
*/
|
||||
ent?: string[];
|
||||
|
||||
/**
|
||||
* Standard expiry in epoch seconds
|
||||
*/
|
||||
exp: number;
|
||||
|
||||
/**
|
||||
* Standard issue time in epoch seconds
|
||||
*/
|
||||
iat: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user