-
Notifications
You must be signed in to change notification settings - Fork 1
feat: classify authentication failure scopes #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { HttpErrorResponse } from '@angular/common/http'; | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { getAuthFailureScope, isIdentityAuthFailure } from './auth-failure'; | ||
|
|
||
| describe('auth failure protocol', () => { | ||
| it.each(['identity', 'reauthentication', 'credential'] as const)('reads %s from an HTTP 401', (scope) => { | ||
| const code = scope === 'identity' ? 'AUTH_IDENTITY_INVALID' : 'DOMAIN_CODE'; | ||
| const error = new HttpErrorResponse({ | ||
| status: 401, | ||
| error: { | ||
| statusCode: 401, | ||
| message: 'Unauthorized', | ||
| code, | ||
| authFailureScope: scope, | ||
| }, | ||
| }); | ||
|
|
||
| expect(getAuthFailureScope(error)).toBe(scope); | ||
| expect(isIdentityAuthFailure(error)).toBe(scope === 'identity'); | ||
| }); | ||
|
|
||
| it('does not infer a destructive identity failure from a legacy status alone', () => { | ||
| expect(getAuthFailureScope(new HttpErrorResponse({ status: 401 }))).toBeNull(); | ||
| expect(isIdentityAuthFailure({ status: 403, error: { authFailureScope: 'identity' } })).toBe(false); | ||
| }); | ||
|
|
||
| it('rejects a mismatched body status or an identity scope without the standard identity code', () => { | ||
| expect( | ||
| getAuthFailureScope({ | ||
| status: 401, | ||
| error: { statusCode: 403, code: 'AUTH_IDENTITY_INVALID', authFailureScope: 'identity' }, | ||
| }), | ||
| ).toBeNull(); | ||
| expect( | ||
| getAuthFailureScope({ | ||
| status: 403, | ||
| error: { statusCode: 403, code: 'BUSINESS_FORBIDDEN', authFailureScope: 'identity' }, | ||
| }), | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it('accepts a body-like value for non-Angular consumers', () => { | ||
| expect( | ||
| getAuthFailureScope({ | ||
| statusCode: 401, | ||
| message: 'Unauthorized', | ||
| code: 'AUTH_IDENTITY_INVALID', | ||
| authFailureScope: 'identity', | ||
| }), | ||
| ).toBe('identity'); | ||
| }); | ||
|
|
||
| it('accepts an explicitly tagged historical 403 identity failure', () => { | ||
| expect( | ||
| isIdentityAuthFailure( | ||
| new HttpErrorResponse({ | ||
| status: 403, | ||
| error: { | ||
| statusCode: 403, | ||
| message: 'Forbidden resource', | ||
| code: 'AUTH_IDENTITY_INVALID', | ||
| authFailureScope: 'identity', | ||
| }, | ||
| }), | ||
| ), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it.each(['reauthentication', 'credential'] as const)('rejects %s on the legacy 403 exception', (scope) => { | ||
| expect( | ||
| getAuthFailureScope({ | ||
| status: 403, | ||
| error: { | ||
| statusCode: 403, | ||
| message: 'Forbidden', | ||
| code: 'DOMAIN_CODE', | ||
| authFailureScope: scope, | ||
| }, | ||
| }), | ||
| ).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||
| /** | ||||||||
| * Shared lifecycle boundary carried by an authentication failure response. | ||||||||
| * | ||||||||
| * Only `identity` invalidates the application's global authenticated identity. | ||||||||
| * The other scopes retain it and let the owning feature handle recovery. | ||||||||
| */ | ||||||||
| export const AUTH_FAILURE_SCOPES = { | ||||||||
| identity: 'identity', | ||||||||
| reauthentication: 'reauthentication', | ||||||||
| credential: 'credential', | ||||||||
| } as const; | ||||||||
|
|
||||||||
| export type AuthFailureScope = (typeof AUTH_FAILURE_SCOPES)[keyof typeof AUTH_FAILURE_SCOPES]; | ||||||||
|
|
||||||||
| export const AUTH_IDENTITY_INVALID_CODE = 'AUTH_IDENTITY_INVALID'; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 公開定数にドキュメントコメントが付いていない 公開APIとして公開される定数 リポジトリ規約違反の内訳同ファイルの
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||
|
|
||||||||
| export interface AuthFailureBody { | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 公開インターフェースにドキュメントコメントが付いていない 公開APIとして公開されるインターフェース リポジトリ規約違反の内訳AGENTS.mdの「Every public class, function, and type must have a JSDoc comment.」に反し、
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||
| statusCode: 401 | 403; | ||||||||
| message: string; | ||||||||
| code: string; | ||||||||
| authFailureScope: AuthFailureScope; | ||||||||
| } | ||||||||
|
|
||||||||
| type UnknownRecord = Record<string, unknown>; | ||||||||
|
|
||||||||
| const isRecord = (value: unknown): value is UnknownRecord => typeof value === 'object' && value !== null; | ||||||||
|
|
||||||||
| const isAuthFailureScope = (value: unknown): value is AuthFailureScope => | ||||||||
| Object.values(AUTH_FAILURE_SCOPES).some((scope) => scope === value); | ||||||||
|
|
||||||||
| /** | ||||||||
| * Reads the explicit auth-failure scope from an Angular `HttpErrorResponse` or | ||||||||
| * from a body-like value. Tagged historical 403 identity responses are | ||||||||
| * supported for installed-client compatibility. Untagged 401/403 responses | ||||||||
| * intentionally return `null`. | ||||||||
| */ | ||||||||
| export const getAuthFailureScope = (error: unknown): AuthFailureScope | null => { | ||||||||
| if (!isRecord(error)) return null; | ||||||||
|
|
||||||||
| const body = isRecord(error['error']) ? error['error'] : error; | ||||||||
| const status = typeof error['status'] === 'number' ? error['status'] : body['statusCode']; | ||||||||
| if (status !== 401 && status !== 403) return null; | ||||||||
| if (body['statusCode'] !== status || typeof body['code'] !== 'string') return null; | ||||||||
|
|
||||||||
| const scope = body['authFailureScope']; | ||||||||
| if (!isAuthFailureScope(scope)) return null; | ||||||||
| if (status === 403 && scope !== AUTH_FAILURE_SCOPES.identity) return null; | ||||||||
| if (scope === AUTH_FAILURE_SCOPES.identity && body['code'] !== AUTH_IDENTITY_INVALID_CODE) return null; | ||||||||
| return scope; | ||||||||
| }; | ||||||||
|
|
||||||||
| /** True only for an explicitly tagged global identity failure. */ | ||||||||
| export const isIdentityAuthFailure = (error: unknown): boolean => getAuthFailureScope(error) === AUTH_FAILURE_SCOPES.identity; | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 公開型にドキュメントコメントが付いていない
公開APIとして公開される型
AuthFailureScope(projects/kit/src/lib/http/auth-failure.ts:13)にJSDocコメントが付いていないため、リポジトリの必須ルールに違反しています。Impact: ライブラリ利用者向けのAPIドキュメントが欠落します。
リポジトリ規約違反の内訳
AGENTS.mdの「Every public class, function, and type must have a JSDoc comment.」および同一リポジトリの既存慣習(
projects/kit/src/lib/auth/auth-access.service.ts:5-6のKitAuthAccessModeなどは全てJSDoc付き)に反し、export type AuthFailureScopeにJSDocがありません。public-api.ts経由で公開されます。Was this helpful? React with 👍 or 👎 to provide feedback.