Browse Source

Task/improve type safety in auth and user services (#7630)

Improve type safety
pull/7645/head^2
Kenrick Tandrian 2 days ago
committed by GitHub
parent
commit
39ba6b98e6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 28
      apps/api/src/app/auth/api-key.strategy.ts
  2. 21
      apps/api/src/app/auth/auth.module.ts
  3. 31
      apps/api/src/app/auth/oidc-state.store.ts
  4. 14
      apps/api/src/app/auth/oidc.strategy.ts
  5. 9
      apps/api/src/app/auth/web-auth.service.ts
  6. 13
      apps/api/src/app/user/user.service.ts

28
apps/api/src/app/auth/api-key.strategy.ts

@ -56,22 +56,20 @@ export class ApiKeyStrategy extends PassportStrategy(
} }
private async validateApiKey(apiKey: string) { private async validateApiKey(apiKey: string) {
if (!apiKey) { if (apiKey) {
throw new HttpException( try {
getReasonPhrase(StatusCodes.UNAUTHORIZED), const { id } = await this.apiKeyService.getUserByApiKey(apiKey);
StatusCodes.UNAUTHORIZED const user = await this.userService.user({ id });
);
}
try {
const { id } = await this.apiKeyService.getUserByApiKey(apiKey);
return this.userService.user({ id }); if (user) {
} catch { return user;
throw new HttpException( }
getReasonPhrase(StatusCodes.UNAUTHORIZED), } catch {}
StatusCodes.UNAUTHORIZED
);
} }
throw new HttpException(
getReasonPhrase(StatusCodes.UNAUTHORIZED),
StatusCodes.UNAUTHORIZED
);
} }
} }

21
apps/api/src/app/auth/auth.module.ts

@ -69,7 +69,7 @@ import { OidcStrategy } from './oidc.strategy';
const issuer = configurationService.get('OIDC_ISSUER'); const issuer = configurationService.get('OIDC_ISSUER');
const scope = configurationService.get('OIDC_SCOPE'); const scope = configurationService.get('OIDC_SCOPE');
const callbackUrl = const callbackURL =
configurationService.get('OIDC_CALLBACK_URL') || configurationService.get('OIDC_CALLBACK_URL') ||
`${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`;
@ -114,15 +114,26 @@ import { OidcStrategy } from './oidc.strategy';
} }
} }
const clientID = configurationService.get('OIDC_CLIENT_ID');
const clientSecret = configurationService.get('OIDC_CLIENT_SECRET');
if (!clientID || !clientSecret || !issuer) {
logger.error(
'OIDC configuration incomplete: issuer, clientID, or clientSecret missing'
);
throw new Error('OIDC configuration incomplete');
}
const options: StrategyOptions = { const options: StrategyOptions = {
authorizationURL, authorizationURL,
callbackURL,
clientID,
clientSecret,
issuer, issuer,
scope, scope,
tokenURL, tokenURL,
userInfoURL, userInfoURL
callbackURL: callbackUrl,
clientID: configurationService.get('OIDC_CLIENT_ID'),
clientSecret: configurationService.get('OIDC_CLIENT_SECRET')
}; };
return new OidcStrategy(authService, options); return new OidcStrategy(authService, options);

31
apps/api/src/app/auth/oidc-state.store.ts

@ -1,17 +1,24 @@
import type { Request } from 'express';
import ms from 'ms'; import ms from 'ms';
import type {
SessionStore,
SessionStoreCallback,
SessionStoreContext,
SessionVerifyCallback
} from 'passport-openidconnect';
/** /**
* Custom state store for OIDC authentication that doesn't rely on express-session. * Custom state store for OIDC authentication that doesn't rely on express-session.
* This store manages OAuth2 state parameters in memory with automatic cleanup. * This store manages OAuth2 state parameters in memory with automatic cleanup.
*/ */
export class OidcStateStore { export class OidcStateStore implements SessionStore {
private readonly STATE_EXPIRY_MS = ms('10 minutes'); private readonly STATE_EXPIRY_MS = ms('10 minutes');
private stateMap = new Map< private stateMap = new Map<
string, string,
{ {
appState?: unknown; appState?: unknown;
ctx: { issued?: Date; maxAge?: number; nonce?: string }; ctx: SessionStoreContext;
meta?: unknown; meta?: unknown;
timestamp: number; timestamp: number;
} }
@ -19,14 +26,13 @@ export class OidcStateStore {
/** /**
* Store request state. * Store request state.
* Signature matches passport-openidconnect SessionStore
*/ */
public store( public store(
_req: unknown, _req: Request,
_meta: unknown, ctx: SessionStoreContext,
appState: unknown, appState: unknown,
ctx: { maxAge?: number; nonce?: string; issued?: Date }, meta: unknown,
callback: (err: Error | null, handle?: string) => void callback: SessionStoreCallback
) { ) {
try { try {
// Generate a unique handle for this state // Generate a unique handle for this state
@ -35,7 +41,7 @@ export class OidcStateStore {
this.stateMap.set(handle, { this.stateMap.set(handle, {
appState, appState,
ctx, ctx,
meta: _meta, meta,
timestamp: Date.now() timestamp: Date.now()
}); });
@ -50,16 +56,11 @@ export class OidcStateStore {
/** /**
* Verify request state. * Verify request state.
* Signature matches passport-openidconnect SessionStore
*/ */
public verify( public verify(
_req: unknown, _req: Request,
handle: string, handle: string,
callback: ( callback: SessionVerifyCallback
err: Error | null,
appState?: unknown,
ctx?: { maxAge?: number; nonce?: string; issued?: Date }
) => void
) { ) {
try { try {
const data = this.stateMap.get(handle); const data = this.stateMap.get(handle);

14
apps/api/src/app/auth/oidc.strategy.ts

@ -15,10 +15,10 @@ import { OidcStateStore } from './oidc-state.store';
@Injectable() @Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private readonly logger = new Logger(OidcStrategy.name);
private static readonly stateStore = new OidcStateStore(); private static readonly stateStore = new OidcStateStore();
private readonly logger = new Logger(OidcStrategy.name);
public constructor( public constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
options: StrategyOptions options: StrategyOptions
@ -48,11 +48,6 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
params?.sub ?? params?.sub ??
context?.claims?.sub; context?.claims?.sub;
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
if (!thirdPartyId) { if (!thirdPartyId) {
this.logger.error( this.logger.error(
`Missing subject identifier in OIDC response from ${issuer}` `Missing subject identifier in OIDC response from ${issuer}`
@ -61,6 +56,11 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
throw new Error('Missing subject identifier in OIDC response'); throw new Error('Missing subject identifier in OIDC response');
} }
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
return { jwt }; return { jwt };
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);

9
apps/api/src/app/auth/web-auth.service.ts

@ -99,6 +99,11 @@ export class WebAuthService {
): Promise<AuthDeviceDto> { ): Promise<AuthDeviceDto> {
const user = this.request.user; const user = this.request.user;
const expectedChallenge = user.authChallenge; const expectedChallenge = user.authChallenge;
if (!expectedChallenge) {
throw new Error('Missing authentication challenge');
}
let verification: VerifiedRegistrationResponse; let verification: VerifiedRegistrationResponse;
try { try {
@ -213,7 +218,7 @@ export class WebAuthService {
id: isoBase64URL.fromBuffer(device.credentialId), id: isoBase64URL.fromBuffer(device.credentialId),
publicKey: device.credentialPublicKey publicKey: device.credentialPublicKey
}, },
expectedChallenge: `${user.authChallenge}`, expectedChallenge: `${user?.authChallenge}`,
expectedOrigin: this.expectedOrigin, expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID, expectedRPID: this.rpID,
requireUserVerification: false, requireUserVerification: false,
@ -243,7 +248,7 @@ export class WebAuthService {
}); });
return this.jwtService.sign({ return this.jwtService.sign({
id: user.id id: user?.id
}); });
} }

13
apps/api/src/app/user/user.service.ts

@ -171,25 +171,28 @@ export class UserService {
userSettings: settings.settings as UserSettings userSettings: settings.settings as UserSettings
}); });
let referralPartners: ReferralPartner[]; let referralPartners: ReferralPartner[] = [];
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic subscription?.type === SubscriptionType.Basic
) { ) {
referralPartners = await this.propertyService.getByKey<ReferralPartner[]>( referralPartners = await this.propertyService.getByKey<ReferralPartner[]>(
PROPERTY_REFERRAL_PARTNERS PROPERTY_REFERRAL_PARTNERS
); );
} }
let systemMessage: SystemMessage; let systemMessage: SystemMessage | undefined;
const systemMessageProperty = const systemMessageProperty =
await this.propertyService.getByKey<SystemMessage>( await this.propertyService.getByKey<SystemMessage>(
PROPERTY_SYSTEM_MESSAGE PROPERTY_SYSTEM_MESSAGE
); );
if (systemMessageProperty?.targetGroups?.includes(subscription?.type)) { if (
subscription?.type &&
systemMessageProperty?.targetGroups?.includes(subscription.type)
) {
systemMessage = systemMessageProperty; systemMessage = systemMessageProperty;
} }
@ -197,7 +200,7 @@ export class UserService {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic subscription?.type === SubscriptionType.Basic
) { ) {
tags = tags.filter(({ id }) => { tags = tags.filter(({ id }) => {
return [TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id); return [TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id);

Loading…
Cancel
Save