@chipmobilesdk/rn-auth 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +239 -113
- package/README.md +221 -190
- package/package.json +72 -54
- package/skills/sdk-rn-auth/SKILL.md +65 -63
- package/src/access/accessTier.ts +88 -88
- package/src/app-auth.ts +6 -0
- package/src/config/types.ts +154 -154
- package/src/config/validateConfig.ts +114 -108
- package/src/core/authMachine.ts +380 -364
- package/src/core/profile.ts +57 -60
- package/src/core/session.ts +105 -112
- package/src/diagnostics/errors.ts +99 -85
- package/src/diagnostics/messageKeys.ts +72 -61
- package/src/index.ts +72 -83
- package/src/keychain.ts +6 -0
- package/src/oidc/authClient.ts +236 -199
- package/src/oidc/classifyError.ts +76 -76
- package/src/oidc/socialRouting.ts +66 -73
- package/src/react/AuthProvider.tsx +30 -47
- package/src/react/hooks.ts +103 -91
- package/src/testing/conformance.ts +177 -0
- package/src/testing/index.ts +30 -0
- package/src/tokens/accessToken.ts +159 -164
- package/src/tokens/secureStore.ts +85 -52
|
@@ -1,63 +1,65 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sdk-rn-auth
|
|
3
|
-
description: "Authentication via @chipmobilesdk/rn-auth: OIDC/AppAuth login, keychain token storage, access tiers, auth error handling. Use when wiring sign-in, storing tokens securely, gating features by tier, or configuring a tenant/provider."
|
|
4
|
-
metadata:
|
|
5
|
-
package: "@chipmobilesdk/rn-auth"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
1
|
+
---
|
|
2
|
+
name: sdk-rn-auth
|
|
3
|
+
description: "Authentication via @chipmobilesdk/rn-auth: OIDC/AppAuth login, keychain token storage, access tiers, auth error handling. Use when wiring sign-in, storing tokens securely, gating features by tier, or configuring a tenant/provider."
|
|
4
|
+
metadata:
|
|
5
|
+
package: "@chipmobilesdk/rn-auth"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
disable-model-invocation: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# @chipmobilesdk/rn-auth
|
|
11
|
+
|
|
12
|
+
OIDC authentication with secure token storage and an access-tier model for
|
|
13
|
+
feature gating.
|
|
14
|
+
|
|
15
|
+
## Use this when
|
|
16
|
+
|
|
17
|
+
- Wiring sign-in / sign-out
|
|
18
|
+
- Persisting tokens (must be keychain, never plain storage)
|
|
19
|
+
- Gating a feature on the user's access tier
|
|
20
|
+
- Mapping an auth failure to a user-facing message
|
|
21
|
+
- Onboarding a new tenant or identity provider
|
|
22
|
+
|
|
23
|
+
## Configuration is a contract, not options
|
|
24
|
+
|
|
25
|
+
Tenant and provider setup has prerequisites that fail at runtime, not at build time.
|
|
26
|
+
Before configuring anything, walk `## Configuration contract (LIB-010)` and
|
|
27
|
+
`## Tenant / provider prerequisite checklist (LIB-018)` in the README. Skipping the
|
|
28
|
+
checklist is the usual cause of "login works locally, fails in the new environment".
|
|
29
|
+
|
|
30
|
+
## Fail closed
|
|
31
|
+
|
|
32
|
+
`failClosedTier` exists because an unknown tier must deny access, not grant it. When
|
|
33
|
+
tier evaluation is unavailable, deny. Never default an unresolved tier to a permissive
|
|
34
|
+
value.
|
|
35
|
+
|
|
36
|
+
## Public API
|
|
37
|
+
|
|
38
|
+
Runtime:
|
|
39
|
+
|
|
40
|
+
- `createAppAuthClient(...)` — the OIDC client
|
|
41
|
+
- `createKeychainSecureStore(...)` — token persistence; `AUTH_STORAGE_KEY`
|
|
42
|
+
- `evaluateAccessTier(...)`, `failClosedTier` — feature gating
|
|
43
|
+
- `AuthProvider` — React context provider
|
|
44
|
+
|
|
45
|
+
Testing (`@chipmobilesdk/rn-auth/testing`): `createMemorySecureStore`,
|
|
46
|
+
`runSecureTokenStoreConformance`, `runOidcAuthClientConformance` — README `## Testing`.
|
|
47
|
+
|
|
48
|
+
Types: `SecureTokenStore`, `AuthProviderProps`, `AuthErrorCode`, `AuthErrorAudience`,
|
|
49
|
+
`AuthMessageMap`, `AuthMessageKey`
|
|
50
|
+
|
|
51
|
+
`AuthErrorAudience` distinguishes what may be shown to a user from what is diagnostic
|
|
52
|
+
only — never render a diagnostic-audience message in the UI.
|
|
53
|
+
|
|
54
|
+
## Where the detail lives
|
|
55
|
+
|
|
56
|
+
Read one section of `node_modules/@chipmobilesdk/rn-auth/README.md`:
|
|
57
|
+
|
|
58
|
+
| Need | Section |
|
|
59
|
+
|---|---|
|
|
60
|
+
| First integration | `## Install (consuming app)`, `## Quick start` |
|
|
61
|
+
| Required config fields | `## Configuration contract (LIB-010)` |
|
|
62
|
+
| New tenant or IdP | `## Tenant / provider prerequisite checklist (LIB-018)` |
|
|
63
|
+
| Token handling, logging rules | `## Security & privacy` |
|
|
64
|
+
| Full export list | `## Public API` |
|
|
65
|
+
| Version/peer constraints | `## Compatibility and validation` |
|
package/src/access/accessTier.ts
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Access-tier evaluation (FR-010/FR-011/FR-019/FR-020, R6).
|
|
3
|
-
*
|
|
4
|
-
* Pure function over the authenticated token's claims and the app-specific
|
|
5
|
-
* mapping. Object ids are authoritative; display names never grant access.
|
|
6
|
-
* Missing, stale, ambiguous, or group-overage claims fail closed (premium
|
|
7
|
-
* denied, verification = 'unverified').
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { AccessTier, AccessTierMapping, ClaimMatcher } from '../config/types';
|
|
11
|
-
|
|
12
|
-
/** The fail-closed default: free + unverified. */
|
|
13
|
-
export function failClosedTier(): AccessTier {
|
|
14
|
-
return { tier: 'free', source: 'default', verification: 'unverified' };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function toStringArray(value: unknown): string[] {
|
|
18
|
-
if (Array.isArray(value)) {
|
|
19
|
-
return value.filter((entry): entry is string => typeof entry === 'string');
|
|
20
|
-
}
|
|
21
|
-
if (typeof value === 'string') {
|
|
22
|
-
return [value];
|
|
23
|
-
}
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Entra emits a groups overage indicator (`_claim_names.groups` or `hasgroups`)
|
|
29
|
-
* instead of inline groups when the list is too large. Reading the full list
|
|
30
|
-
* would require Graph (out of scope, FR-012), so overage fails closed.
|
|
31
|
-
*/
|
|
32
|
-
function hasGroupOverage(claims: Record<string, unknown>): boolean {
|
|
33
|
-
if (claims.hasgroups === true) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
const claimNames = claims._claim_names as Record<string, unknown> | undefined;
|
|
37
|
-
if (claimNames && typeof claimNames === 'object' && 'groups' in claimNames) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function matchesClaim(claims: Record<string, unknown>, matcher: ClaimMatcher): boolean {
|
|
44
|
-
const value = claims[matcher.claim];
|
|
45
|
-
if (Array.isArray(value)) {
|
|
46
|
-
return value.includes(matcher.equals);
|
|
47
|
-
}
|
|
48
|
-
return value === matcher.equals;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Evaluate the access tier, failing closed when entitlement is unverifiable. */
|
|
52
|
-
export function evaluateAccessTier(
|
|
53
|
-
claims: Record<string, unknown>,
|
|
54
|
-
mapping: AccessTierMapping,
|
|
55
|
-
now: number = Date.now(),
|
|
56
|
-
): AccessTier {
|
|
57
|
-
// Object ids first (authoritative). Display names are never consulted.
|
|
58
|
-
const groups = toStringArray(claims.groups);
|
|
59
|
-
if (
|
|
60
|
-
mapping.premiumGroupObjectIds.length > 0 &&
|
|
61
|
-
groups.some(group => mapping.premiumGroupObjectIds.includes(group))
|
|
62
|
-
) {
|
|
63
|
-
return { tier: 'premium', source: 'group', verification: 'verified', lastRefreshAt: now };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Roles next.
|
|
67
|
-
const roles = toStringArray(claims.roles);
|
|
68
|
-
if (
|
|
69
|
-
mapping.premiumRoleNames?.length &&
|
|
70
|
-
roles.some(role => mapping.premiumRoleNames!.includes(role))
|
|
71
|
-
) {
|
|
72
|
-
return { tier: 'premium', source: 'role', verification: 'verified', lastRefreshAt: now };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Custom claim matchers last.
|
|
76
|
-
if (mapping.premiumClaimMatchers?.some(matcher => matchesClaim(claims, matcher))) {
|
|
77
|
-
return { tier: 'premium', source: 'claim', verification: 'verified', lastRefreshAt: now };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// No premium indicator matched. If group claims are unavailable due to
|
|
81
|
-
// overage and premium depends on groups, we cannot verify → fail closed.
|
|
82
|
-
if (mapping.premiumGroupObjectIds.length > 0 && hasGroupOverage(claims)) {
|
|
83
|
-
return { tier: 'free', source: 'default', verification: 'unverified', lastRefreshAt: now };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Verified free: claims were present and simply did not grant premium.
|
|
87
|
-
return { tier: 'free', source: 'default', verification: 'verified', lastRefreshAt: now };
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Access-tier evaluation (FR-010/FR-011/FR-019/FR-020, R6).
|
|
3
|
+
*
|
|
4
|
+
* Pure function over the authenticated token's claims and the app-specific
|
|
5
|
+
* mapping. Object ids are authoritative; display names never grant access.
|
|
6
|
+
* Missing, stale, ambiguous, or group-overage claims fail closed (premium
|
|
7
|
+
* denied, verification = 'unverified').
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AccessTier, AccessTierMapping, ClaimMatcher } from '../config/types';
|
|
11
|
+
|
|
12
|
+
/** The fail-closed default: free + unverified. */
|
|
13
|
+
export function failClosedTier(): AccessTier {
|
|
14
|
+
return { tier: 'free', source: 'default', verification: 'unverified' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toStringArray(value: unknown): string[] {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return value.filter((entry): entry is string => typeof entry === 'string');
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
return [value];
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Entra emits a groups overage indicator (`_claim_names.groups` or `hasgroups`)
|
|
29
|
+
* instead of inline groups when the list is too large. Reading the full list
|
|
30
|
+
* would require Graph (out of scope, FR-012), so overage fails closed.
|
|
31
|
+
*/
|
|
32
|
+
function hasGroupOverage(claims: Record<string, unknown>): boolean {
|
|
33
|
+
if (claims.hasgroups === true) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const claimNames = claims._claim_names as Record<string, unknown> | undefined;
|
|
37
|
+
if (claimNames && typeof claimNames === 'object' && 'groups' in claimNames) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function matchesClaim(claims: Record<string, unknown>, matcher: ClaimMatcher): boolean {
|
|
44
|
+
const value = claims[matcher.claim];
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.includes(matcher.equals);
|
|
47
|
+
}
|
|
48
|
+
return value === matcher.equals;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Evaluate the access tier, failing closed when entitlement is unverifiable. */
|
|
52
|
+
export function evaluateAccessTier(
|
|
53
|
+
claims: Record<string, unknown>,
|
|
54
|
+
mapping: AccessTierMapping,
|
|
55
|
+
now: number = Date.now(),
|
|
56
|
+
): AccessTier {
|
|
57
|
+
// Object ids first (authoritative). Display names are never consulted.
|
|
58
|
+
const groups = toStringArray(claims.groups);
|
|
59
|
+
if (
|
|
60
|
+
mapping.premiumGroupObjectIds.length > 0 &&
|
|
61
|
+
groups.some(group => mapping.premiumGroupObjectIds.includes(group))
|
|
62
|
+
) {
|
|
63
|
+
return { tier: 'premium', source: 'group', verification: 'verified', lastRefreshAt: now };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Roles next.
|
|
67
|
+
const roles = toStringArray(claims.roles);
|
|
68
|
+
if (
|
|
69
|
+
mapping.premiumRoleNames?.length &&
|
|
70
|
+
roles.some(role => mapping.premiumRoleNames!.includes(role))
|
|
71
|
+
) {
|
|
72
|
+
return { tier: 'premium', source: 'role', verification: 'verified', lastRefreshAt: now };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Custom claim matchers last.
|
|
76
|
+
if (mapping.premiumClaimMatchers?.some(matcher => matchesClaim(claims, matcher))) {
|
|
77
|
+
return { tier: 'premium', source: 'claim', verification: 'verified', lastRefreshAt: now };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// No premium indicator matched. If group claims are unavailable due to
|
|
81
|
+
// overage and premium depends on groups, we cannot verify → fail closed.
|
|
82
|
+
if (mapping.premiumGroupObjectIds.length > 0 && hasGroupOverage(claims)) {
|
|
83
|
+
return { tier: 'free', source: 'default', verification: 'unverified', lastRefreshAt: now };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Verified free: claims were present and simply did not grant premium.
|
|
87
|
+
return { tier: 'free', source: 'default', verification: 'verified', lastRefreshAt: now };
|
|
88
|
+
}
|
package/src/app-auth.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chipmobilesdk/rn-auth/app-auth`: the default OIDC client over
|
|
3
|
+
* react-native-app-auth (AUTH-09). Its own entrypoint, so an app that injects its
|
|
4
|
+
* own client never reaches this module.
|
|
5
|
+
*/
|
|
6
|
+
export { createAppAuthClient, type AppAuthClientOptions } from './oidc/authClient';
|
package/src/config/types.ts
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public type contracts for @chipmobilesdk/rn-auth. Shapes follow
|
|
3
|
-
* specs/003-mobile-entra-auth/data-model.md.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { SecureTokenStore } from '../tokens/secureStore';
|
|
7
|
-
import type { OidcAuthClient } from '../oidc/authClient';
|
|
8
|
-
import type { AuthError, AuthErrorCode } from '../diagnostics/errors';
|
|
9
|
-
import type { AuthMessageKey, AuthMessageMap } from '../diagnostics/messageKeys';
|
|
10
|
-
|
|
11
|
-
export type ProviderId = 'google' | 'apple' | 'facebook';
|
|
12
|
-
export type DevicePlatform = 'ios' | 'android';
|
|
13
|
-
|
|
14
|
-
/** Provider Setup Configuration — non-secret public values only (FR-016/FR-017). */
|
|
15
|
-
export interface ProviderConfig {
|
|
16
|
-
id: ProviderId;
|
|
17
|
-
platforms: DevicePlatform[];
|
|
18
|
-
/** External ID `domain_hint` routing the user flow to this IdP (R3). */
|
|
19
|
-
domainHint: string;
|
|
20
|
-
/** Message key for the button label (Continue with Google / Apple). */
|
|
21
|
-
labelKey: AuthMessageKey;
|
|
22
|
-
available?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** One configured protected resource for delegated token requests (FR-007). */
|
|
26
|
-
export interface ResourceConfig {
|
|
27
|
-
key: string;
|
|
28
|
-
scopes: string[];
|
|
29
|
-
displayName?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ClaimMatcher {
|
|
33
|
-
claim: string;
|
|
34
|
-
equals: string | number | boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** App-specific mapping from tenant indicators to access tiers (FR-019/FR-020). */
|
|
38
|
-
export interface AccessTierMapping {
|
|
39
|
-
/** Stable group object ids granting premium — authoritative (FR-020). */
|
|
40
|
-
premiumGroupObjectIds: string[];
|
|
41
|
-
premiumRoleNames?: string[];
|
|
42
|
-
premiumClaimMatchers?: ClaimMatcher[];
|
|
43
|
-
/** Diagnostics-only object-id → name map; never used for decisions (FR-020). */
|
|
44
|
-
groupDisplayNames?: Record<string, string>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** App-supplied, public-only setup input (FR-015/FR-016/FR-017). */
|
|
48
|
-
export interface AuthConfig {
|
|
49
|
-
authority: string;
|
|
50
|
-
clientId: string;
|
|
51
|
-
redirectUrl: string;
|
|
52
|
-
scopes: string[];
|
|
53
|
-
providers: ProviderConfig[];
|
|
54
|
-
resources?: ResourceConfig[];
|
|
55
|
-
accessTierMapping: AccessTierMapping;
|
|
56
|
-
secureStore: SecureTokenStore;
|
|
57
|
-
authClient?: OidcAuthClient;
|
|
58
|
-
messages?: Partial<AuthMessageMap>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Advanced/testing options. `platform` overrides the detected device platform. */
|
|
62
|
-
export interface CreateAuthOptions {
|
|
63
|
-
platform?: DevicePlatform;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type AuthStatus =
|
|
67
|
-
| 'loading'
|
|
68
|
-
| 'signed-out'
|
|
69
|
-
| 'authenticating'
|
|
70
|
-
| 'authenticated'
|
|
71
|
-
| 'token-refreshing'
|
|
72
|
-
| 'access-denied'
|
|
73
|
-
| 'cancelled'
|
|
74
|
-
| 'error';
|
|
75
|
-
|
|
76
|
-
export type RestoreStatus = 'pending' | 'restored' | 'unavailable' | 'offline';
|
|
77
|
-
|
|
78
|
-
/** Customer User / app-visible profile (FR-006). */
|
|
79
|
-
export interface Profile {
|
|
80
|
-
tenantUserId: string;
|
|
81
|
-
displayName?: string;
|
|
82
|
-
email?: string;
|
|
83
|
-
provider: string;
|
|
84
|
-
claims: Record<string, unknown>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface ProviderIdentity {
|
|
88
|
-
provider: string;
|
|
89
|
-
subject: string;
|
|
90
|
-
emailBehavior: 'direct' | 'private-relay' | 'none';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Access Tier — local interpretation of tenant indicators (FR-010). */
|
|
94
|
-
export interface AccessTier {
|
|
95
|
-
tier: 'free' | 'premium';
|
|
96
|
-
source: 'group' | 'role' | 'claim' | 'default';
|
|
97
|
-
verification: 'verified' | 'unverified';
|
|
98
|
-
lastRefreshAt?: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export type AccessTokenStatus = 'success' | 'denied' | 'unavailable' | 'error';
|
|
102
|
-
|
|
103
|
-
/** Access Token Request result (FR-007). */
|
|
104
|
-
export interface AccessTokenResult {
|
|
105
|
-
resourceKey: string;
|
|
106
|
-
status: AccessTokenStatus;
|
|
107
|
-
token?: string;
|
|
108
|
-
expiresAt?: number;
|
|
109
|
-
errorCode?: AuthErrorCode;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Single active app-visible authenticated state (FR-005/FR-022). */
|
|
113
|
-
export interface AuthState {
|
|
114
|
-
status: AuthStatus;
|
|
115
|
-
profile?: Profile;
|
|
116
|
-
accessTier?: AccessTier;
|
|
117
|
-
restore: RestoreStatus;
|
|
118
|
-
error?: AuthError;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Result of a premium-only gate check (US3). */
|
|
122
|
-
export interface PremiumGateResult {
|
|
123
|
-
allowed: boolean;
|
|
124
|
-
tier: AccessTier;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* What `signOutGlobal` did beyond the device (added in 0.2.2). Local state is
|
|
129
|
-
* cleared either way; `failed` means the tenant session may still be alive, so
|
|
130
|
-
* the next interactive sign-in can complete without a password prompt.
|
|
131
|
-
*/
|
|
132
|
-
export interface GlobalSignOutResult {
|
|
133
|
-
global: 'ended' | 'failed';
|
|
134
|
-
/** Present when `global` is `failed`. Redacted like every AuthError. */
|
|
135
|
-
error?: AuthError;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** The auth controller returned by `createAuth` (single active account). */
|
|
139
|
-
export interface Auth {
|
|
140
|
-
getState(): AuthState;
|
|
141
|
-
subscribe(listener: (state: AuthState) => void): () => void;
|
|
142
|
-
restore(): Promise<AuthState>;
|
|
143
|
-
signIn(providerId: ProviderId): Promise<AuthState>;
|
|
144
|
-
signOut(): Promise<void>;
|
|
145
|
-
signOutGlobal(): Promise<GlobalSignOutResult>;
|
|
146
|
-
requestAccessToken(resourceKey: string): Promise<AccessTokenResult>;
|
|
147
|
-
getAccessTier(): AccessTier;
|
|
148
|
-
/** Gate a premium-only path; enters `access-denied` when unverified (FR-011). */
|
|
149
|
-
requirePremium(): PremiumGateResult;
|
|
150
|
-
/** The first available provider to present on the current platform (UX-004), or undefined. */
|
|
151
|
-
getPlatformProvider(): ProviderConfig | undefined;
|
|
152
|
-
/** All available providers for the current platform, in configuration order (FR-003/FR-004). */
|
|
153
|
-
getPlatformProviders(): ProviderConfig[];
|
|
154
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Public type contracts for @chipmobilesdk/rn-auth. Shapes follow
|
|
3
|
+
* specs/003-mobile-entra-auth/data-model.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SecureTokenStore } from '../tokens/secureStore';
|
|
7
|
+
import type { OidcAuthClient } from '../oidc/authClient';
|
|
8
|
+
import type { AuthError, AuthErrorCode } from '../diagnostics/errors';
|
|
9
|
+
import type { AuthMessageKey, AuthMessageMap } from '../diagnostics/messageKeys';
|
|
10
|
+
|
|
11
|
+
export type ProviderId = 'google' | 'apple' | 'facebook';
|
|
12
|
+
export type DevicePlatform = 'ios' | 'android';
|
|
13
|
+
|
|
14
|
+
/** Provider Setup Configuration — non-secret public values only (FR-016/FR-017). */
|
|
15
|
+
export interface ProviderConfig {
|
|
16
|
+
id: ProviderId;
|
|
17
|
+
platforms: DevicePlatform[];
|
|
18
|
+
/** External ID `domain_hint` routing the user flow to this IdP (R3). */
|
|
19
|
+
domainHint: string;
|
|
20
|
+
/** Message key for the button label (Continue with Google / Apple). */
|
|
21
|
+
labelKey: AuthMessageKey;
|
|
22
|
+
available?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One configured protected resource for delegated token requests (FR-007). */
|
|
26
|
+
export interface ResourceConfig {
|
|
27
|
+
key: string;
|
|
28
|
+
scopes: string[];
|
|
29
|
+
displayName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ClaimMatcher {
|
|
33
|
+
claim: string;
|
|
34
|
+
equals: string | number | boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** App-specific mapping from tenant indicators to access tiers (FR-019/FR-020). */
|
|
38
|
+
export interface AccessTierMapping {
|
|
39
|
+
/** Stable group object ids granting premium — authoritative (FR-020). */
|
|
40
|
+
premiumGroupObjectIds: string[];
|
|
41
|
+
premiumRoleNames?: string[];
|
|
42
|
+
premiumClaimMatchers?: ClaimMatcher[];
|
|
43
|
+
/** Diagnostics-only object-id → name map; never used for decisions (FR-020). */
|
|
44
|
+
groupDisplayNames?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** App-supplied, public-only setup input (FR-015/FR-016/FR-017). */
|
|
48
|
+
export interface AuthConfig {
|
|
49
|
+
authority: string;
|
|
50
|
+
clientId: string;
|
|
51
|
+
redirectUrl: string;
|
|
52
|
+
scopes: string[];
|
|
53
|
+
providers: ProviderConfig[];
|
|
54
|
+
resources?: ResourceConfig[];
|
|
55
|
+
accessTierMapping: AccessTierMapping;
|
|
56
|
+
secureStore: SecureTokenStore;
|
|
57
|
+
authClient?: OidcAuthClient;
|
|
58
|
+
messages?: Partial<AuthMessageMap>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Advanced/testing options. `platform` overrides the detected device platform. */
|
|
62
|
+
export interface CreateAuthOptions {
|
|
63
|
+
platform?: DevicePlatform;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type AuthStatus =
|
|
67
|
+
| 'loading'
|
|
68
|
+
| 'signed-out'
|
|
69
|
+
| 'authenticating'
|
|
70
|
+
| 'authenticated'
|
|
71
|
+
| 'token-refreshing'
|
|
72
|
+
| 'access-denied'
|
|
73
|
+
| 'cancelled'
|
|
74
|
+
| 'error';
|
|
75
|
+
|
|
76
|
+
export type RestoreStatus = 'pending' | 'restored' | 'unavailable' | 'offline';
|
|
77
|
+
|
|
78
|
+
/** Customer User / app-visible profile (FR-006). */
|
|
79
|
+
export interface Profile {
|
|
80
|
+
tenantUserId: string;
|
|
81
|
+
displayName?: string | undefined;
|
|
82
|
+
email?: string | undefined;
|
|
83
|
+
provider: string;
|
|
84
|
+
claims: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ProviderIdentity {
|
|
88
|
+
provider: string;
|
|
89
|
+
subject: string;
|
|
90
|
+
emailBehavior: 'direct' | 'private-relay' | 'none';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Access Tier — local interpretation of tenant indicators (FR-010). */
|
|
94
|
+
export interface AccessTier {
|
|
95
|
+
tier: 'free' | 'premium';
|
|
96
|
+
source: 'group' | 'role' | 'claim' | 'default';
|
|
97
|
+
verification: 'verified' | 'unverified';
|
|
98
|
+
lastRefreshAt?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type AccessTokenStatus = 'success' | 'denied' | 'unavailable' | 'error';
|
|
102
|
+
|
|
103
|
+
/** Access Token Request result (FR-007). */
|
|
104
|
+
export interface AccessTokenResult {
|
|
105
|
+
resourceKey: string;
|
|
106
|
+
status: AccessTokenStatus;
|
|
107
|
+
token?: string;
|
|
108
|
+
expiresAt?: number;
|
|
109
|
+
errorCode?: AuthErrorCode;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Single active app-visible authenticated state (FR-005/FR-022). */
|
|
113
|
+
export interface AuthState {
|
|
114
|
+
status: AuthStatus;
|
|
115
|
+
profile?: Profile | undefined;
|
|
116
|
+
accessTier?: AccessTier | undefined;
|
|
117
|
+
restore: RestoreStatus;
|
|
118
|
+
error?: AuthError | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Result of a premium-only gate check (US3). */
|
|
122
|
+
export interface PremiumGateResult {
|
|
123
|
+
allowed: boolean;
|
|
124
|
+
tier: AccessTier;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What `signOutGlobal` did beyond the device (added in 0.2.2). Local state is
|
|
129
|
+
* cleared either way; `failed` means the tenant session may still be alive, so
|
|
130
|
+
* the next interactive sign-in can complete without a password prompt.
|
|
131
|
+
*/
|
|
132
|
+
export interface GlobalSignOutResult {
|
|
133
|
+
global: 'ended' | 'failed';
|
|
134
|
+
/** Present when `global` is `failed`. Redacted like every AuthError. */
|
|
135
|
+
error?: AuthError;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The auth controller returned by `createAuth` (single active account). */
|
|
139
|
+
export interface Auth {
|
|
140
|
+
getState(): AuthState;
|
|
141
|
+
subscribe(listener: (state: AuthState) => void): () => void;
|
|
142
|
+
restore(): Promise<AuthState>;
|
|
143
|
+
signIn(providerId: ProviderId): Promise<AuthState>;
|
|
144
|
+
signOut(): Promise<void>;
|
|
145
|
+
signOutGlobal(): Promise<GlobalSignOutResult>;
|
|
146
|
+
requestAccessToken(resourceKey: string): Promise<AccessTokenResult>;
|
|
147
|
+
getAccessTier(): AccessTier;
|
|
148
|
+
/** Gate a premium-only path; enters `access-denied` when unverified (FR-011). */
|
|
149
|
+
requirePremium(): PremiumGateResult;
|
|
150
|
+
/** The first available provider to present on the current platform (UX-004), or undefined. */
|
|
151
|
+
getPlatformProvider(): ProviderConfig | undefined;
|
|
152
|
+
/** All available providers for the current platform, in configuration order (FR-003/FR-004). */
|
|
153
|
+
getPlatformProviders(): ProviderConfig[];
|
|
154
|
+
}
|