forked from stashenergy/react-native-msal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
b2cClient.ts
175 lines (157 loc) · 6.38 KB
/
b2cClient.ts
1
2
3
4
5
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import { Platform } from 'react-native';
import PublicClientApplication from 'react-native-msal';
import type {
MSALAccount,
MSALConfiguration,
MSALInteractiveParams,
MSALResult,
MSALSignoutParams,
MSALSilentParams,
MSALWebviewParams,
} from 'react-native-msal';
export interface B2CPolicies {
signInSignUp: string;
passwordReset?: string;
}
export type B2CConfiguration = Omit<MSALConfiguration, 'auth'> & {
auth: {
clientId: string;
authorityBase: string;
policies: B2CPolicies;
redirectUri?: string;
};
};
export type B2CSignInParams = Omit<MSALInteractiveParams, 'authority'>;
export type B2CSilentParams = Pick<MSALSilentParams, 'scopes' | 'forceRefresh'>;
export type B2CSignOutParams = Pick<MSALSignoutParams, 'signoutFromBrowser' | 'webviewParameters'>;
export class B2CClient {
private static readonly B2C_PASSWORD_CHANGE = 'AADB2C90118';
private static readonly B2C_EXPIRED_GRANT = 'AADB2C90080';
private readonly policyUrls: B2CPolicies;
private pca: PublicClientApplication;
/** Construct a B2CClient object
* @param b2cConfig The configuration object for the B2CClient
*/
constructor(b2cConfig: B2CConfiguration) {
const { authorityBase, policies, ...restOfAuthConfig } = b2cConfig.auth;
this.policyUrls = makePolicyUrls(authorityBase, policies);
// Set the sign in sign up policy as the default authority for the PublicClientApplication (PCA).
const authority = this.policyUrls.signInSignUp;
// We need to provide all authorities we'll be using up front.
// The default authority should be included in this list.
const knownAuthorities = Object.values(this.policyUrls);
this.pca = new PublicClientApplication({
...b2cConfig,
auth: { authority, knownAuthorities, ...restOfAuthConfig },
});
}
public async init() {
await this.pca.init();
return this;
}
/** Initiates an interactive sign-in. If the user clicks "Forgot Password", and a reset password policy
* was provided to the client, it will initiate the password reset flow
*/
public async signIn(params: B2CSignInParams): Promise<MSALResult> {
const isSignedIn = await this.isSignedIn();
if (isSignedIn) {
throw Error('A user is already signed in');
}
try {
// If we don't provide an authority, the PCA will use the one we passed to it when we created it
// (the sign in sign up policy)
const result = await this.pca.acquireToken(params);
if (!result) {
throw new Error('Could not sign in: Result was undefined.');
}
return result;
} catch (error: unknown) {
if (
error instanceof Error &&
error.message.includes(B2CClient.B2C_PASSWORD_CHANGE) &&
this.policyUrls.passwordReset
) {
return await this.resetPassword(params);
} else {
throw error;
}
}
}
/** Gets a token silently. Will only work if the user is already signed in */
public async acquireTokenSilent(params: B2CSilentParams) {
const account = await this.getAccountForPolicy(this.policyUrls.signInSignUp);
if (account) {
// We provide the account that we got when we signed in, with the matching sign in sign up authority
// Which again, we set as the default authority so we don't need to provide it explicitly.
try {
const result = await this.pca.acquireTokenSilent({ ...params, account });
if (!result) {
throw new Error('Could not acquire token silently: Result was undefined.');
}
return result;
} catch (error: unknown) {
if (error instanceof Error && error.message.includes(B2CClient.B2C_EXPIRED_GRANT)) {
await this.pca.signOut({ ...params, account });
return await this.signIn(params);
} else {
throw error;
}
}
} else {
throw Error('Could not find existing account for sign in sign up policy');
}
}
/** Returns true if a user is signed in, false if not */
public async isSignedIn() {
const signInAccount = await this.getAccountForPolicy(this.policyUrls.signInSignUp);
return signInAccount !== undefined;
}
/** Removes all accounts from the device for this app. User will have to sign in again to get a token */
public async signOut(params?: B2CSignOutParams) {
const accounts = await this.pca.getAccounts();
const signOutPromises = accounts.map((account) => this.pca.signOut({ ...params, account }));
await Promise.all(signOutPromises);
return true;
}
private async resetPassword(params: B2CSignInParams) {
const { webviewParameters: wvp, ...rest } = params;
const webviewParameters: MSALWebviewParams = {
...wvp,
// We use an ephemeral session because if we're resetting a password it means the user
// is not using an identity provider, so we don't need a logged-in browser session
ios_prefersEphemeralWebBrowserSession: true,
};
if (this.policyUrls.passwordReset) {
// Because there is no prompt before starting an iOS ephemeral session, it will be quick to
// open and begin before the other one has ended, causing an error saying that only one
// interactive session is allowed at a time. So we have to slow it down a little
if (Platform.OS === 'ios') {
await delay(1000);
}
// Use the password reset policy in the interactive `acquireToken` call
const authority = this.policyUrls.passwordReset;
await this.pca.acquireToken({ ...rest, webviewParameters, authority });
// Sign in again after resetting the password
return await this.signIn(params);
} else {
throw Error('B2CClient missing password reset policy');
}
}
private async getAccountForPolicy(policyUrl: string): Promise<MSALAccount | undefined> {
const policy = policyUrl.split('/').pop();
const accounts = await this.pca.getAccounts();
return accounts.find((account) => account.identifier.includes(policy!.toLowerCase()));
}
}
function makeAuthority(authorityBase: string, policyName: string) {
return `${authorityBase}/${policyName}`;
}
function makePolicyUrls(authorityBase: string, policyNames: B2CPolicies): B2CPolicies {
return Object.entries(policyNames).reduce(
(prev, [key, policyName]) => ({ ...prev, [key]: makeAuthority(authorityBase, policyName) }),
{} as B2CPolicies
);
}
async function delay(ms: number) {
return await new Promise<void>((resolve) => setTimeout(resolve, ms));
}