@authara/react 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @authara/react
2
2
 
3
- Official React buttons for complete Authara Google and Apple authentication
3
+ Official React buttons for complete Authara Google, Apple, and Microsoft authentication
4
4
  flows. The components load the provider SDK, obtain the provider proof, prepare
5
5
  CSRF, call the generated Authara browser client, and return a typed result.
6
6
 
@@ -32,12 +32,12 @@ export function App() {
32
32
  ```
33
33
 
34
34
  The provider owns no user or session state. It coordinates Authara's generated
35
- client and the page-global Google and Apple SDKs.
35
+ client and the external-provider SDKs.
36
36
 
37
37
  ## Login
38
38
 
39
39
  ```tsx
40
- import { AppleButton, GoogleButton } from "@authara/react";
40
+ import { AppleButton, GoogleButton, MicrosoftButton } from "@authara/react";
41
41
 
42
42
  export function Login() {
43
43
  return (
@@ -60,6 +60,16 @@ export function Login() {
60
60
  onError={handleAuthError}
61
61
  onCancel={() => setMessage("Sign in was cancelled.")}
62
62
  />
63
+
64
+ <MicrosoftButton
65
+ action="login"
66
+ audience="app"
67
+ variant="text"
68
+ size="large"
69
+ onSuccess={({ session }) => setCurrentUser(session.user)}
70
+ onError={handleAuthError}
71
+ onCancel={() => setMessage("Sign in was cancelled.")}
72
+ />
63
73
  </div>
64
74
  );
65
75
  }
@@ -82,6 +92,12 @@ default action is `login` and the default audience is `app`.
82
92
  onSuccess={() => refreshAccount()}
83
93
  onError={handleAuthError}
84
94
  />
95
+
96
+ <MicrosoftButton
97
+ action="link"
98
+ onSuccess={() => refreshAccount()}
99
+ onError={handleAuthError}
100
+ />
85
101
  ```
86
102
 
87
103
  Linking uses the current Authara session and therefore does not accept an
@@ -97,6 +113,13 @@ needs a fresh proof.
97
113
  onSuccess={() => retrySensitiveAction()}
98
114
  onError={handleAuthError}
99
115
  />
116
+
117
+ <MicrosoftButton
118
+ action="reauthenticate"
119
+ authenticationChallengeId={challenge.id}
120
+ onSuccess={() => retrySensitiveAction()}
121
+ onError={handleAuthError}
122
+ />
100
123
  ```
101
124
 
102
125
  TypeScript requires `authenticationChallengeId` for reauthentication and
@@ -125,28 +148,43 @@ function handleLoginError(error: AutharaButtonError) {
125
148
  showMessage(error.message);
126
149
  }
127
150
 
128
- {recovery?.proof_methods.includes("google") && (
129
- <GoogleButton
130
- action="recover"
131
- recoveryLinkId={recovery.link_id}
132
- onSuccess={({ session }) => setCurrentUser(session.user)}
133
- onError={handleLoginError}
134
- />
135
- )}
136
-
137
- {recovery?.proof_methods.includes("apple") && (
138
- <AppleButton
139
- action="recover"
140
- recoveryLinkId={recovery.link_id}
141
- onSuccess={({ session }) => setCurrentUser(session.user)}
142
- onError={handleLoginError}
143
- />
144
- )}
151
+ {
152
+ recovery?.proof_methods.includes("google") && (
153
+ <GoogleButton
154
+ action="recover"
155
+ recoveryLinkId={recovery.link_id}
156
+ onSuccess={({ session }) => setCurrentUser(session.user)}
157
+ onError={handleLoginError}
158
+ />
159
+ );
160
+ }
161
+
162
+ {
163
+ recovery?.proof_methods.includes("apple") && (
164
+ <AppleButton
165
+ action="recover"
166
+ recoveryLinkId={recovery.link_id}
167
+ onSuccess={({ session }) => setCurrentUser(session.user)}
168
+ onError={handleLoginError}
169
+ />
170
+ );
171
+ }
172
+
173
+ {
174
+ recovery?.proof_methods.includes("microsoft") && (
175
+ <MicrosoftButton
176
+ action="recover"
177
+ recoveryLinkId={recovery.link_id}
178
+ onSuccess={({ session }) => setCurrentUser(session.user)}
179
+ onError={handleLoginError}
180
+ />
181
+ );
182
+ }
145
183
  ```
146
184
 
147
- This is symmetric: an Apple collision can be proved with an existing Google
148
- identity, and a Google collision can be proved with an existing Apple identity.
149
- Authara never links accounts from an email match alone.
185
+ The flow is symmetric across external providers: a collision can be proved
186
+ with any existing Google, Apple, or Microsoft identity returned in
187
+ `proof_methods`. Authara never links accounts from an email match alone.
150
188
 
151
189
  ## Errors
152
190
 
@@ -196,12 +234,12 @@ unions are generated from Core's `x-authara-error-codes` declarations:
196
234
  - every action includes its declared authorization, request, availability, and
197
235
  internal codes, plus the local CSRF and unknown-code fallbacks
198
236
 
199
- Apple popup cancellation calls `onCancel`, not `onError`. A provider disabled
237
+ Apple and Microsoft popup cancellation calls `onCancel`, not `onError`. A provider disabled
200
238
  in Authara renders `fallback` and calls `onUnavailable`.
201
239
 
202
240
  ## Appearance
203
241
 
204
- Both components share the simple properties:
242
+ All three components share the simple properties:
205
243
 
206
244
  ```tsx
207
245
  <AppleButton
@@ -216,6 +254,8 @@ Both components share the simple properties:
216
254
  size="large"
217
255
  mode="system"
218
256
  />
257
+
258
+ <MicrosoftButton action="login" variant="text" size="large" mode="system" />
219
259
  ```
220
260
 
221
261
  - `variant`: `text` or `icon`
@@ -235,12 +275,13 @@ precedence over these defaults.
235
275
  Google additionally exposes `theme`, `border`, `shape`, `logoAlignment`, and
236
276
  `width`.
237
277
  Apple exposes `color`, `border`, `borderRadius`, `align`, and `width`.
238
- Both providers accept `buttonText` to localize or otherwise override the
278
+ Microsoft exposes `border`, `align`, and `width`. All providers accept
279
+ `buttonText` to localize or otherwise override the
239
280
  visible label and the accessible name of icon-only buttons.
240
281
  Text and icon variants both default to a `12px` corner radius. Apple's
241
282
  `borderRadius` accepts its supported `0`–`50` range; Google's `shape` can still
242
283
  select `pill`, `circle`, `square`, or `rectangular` explicitly.
243
- Both providers use an Authara-rendered, centered logo-and-label structure.
284
+ All providers use an Authara-rendered, centered logo-and-label structure.
244
285
  Google's `light`, `dark`, and `neutral` themes use Google's required colors,
245
286
  spacing, and typography. Make Google Sans Medium available in your application
246
287
  as required by Google's branding guidelines; this package does not inject a
@@ -257,15 +298,16 @@ components:
257
298
  ```tsx
258
299
  <GoogleCredentialButton onCredential={handleGoogleProof} />
259
300
  <AppleCredentialButton onAuthorization={handleAppleProof} />
301
+ <MicrosoftCredentialButton onCredential={handleMicrosoftProof} />
260
302
  ```
261
303
 
262
- Most applications should use `GoogleButton` and `AppleButton`; the lower-level
263
- components deliberately expose Google's one-time code and browser origin or
264
- Apple's code and state.
304
+ Most applications should use `GoogleButton`, `AppleButton`, and
305
+ `MicrosoftButton`; the lower-level components deliberately expose the
306
+ short-lived provider proofs used by Core.
265
307
 
266
308
  ## Real local playground
267
309
 
268
- The Vite playground uses the real Google and Apple scripts and sends real HTTP
310
+ The Vite playground uses the real Google, Apple, and Microsoft SDKs and sends real HTTP
269
311
  requests to Authara. It has no provider or API mocks.
270
312
 
271
313
  For fast component development, start an Authara gateway with Google and/or
@@ -281,6 +323,11 @@ Core reads Google configuration from `AUTHARA_OAUTH_GOOGLE_CLIENT_ID` and
281
323
  receives it. Apple uses `AUTHARA_OAUTH_APPLE_CLIENT_ID`, `AUTHARA_OAUTH_APPLE_TEAM_ID`,
282
324
  `AUTHARA_OAUTH_APPLE_KEY_ID`, `AUTHARA_OAUTH_APPLE_PRIVATE_KEY_BASE64`, and the
283
325
  Apple token-key settings documented by Authara Core.
326
+ Microsoft uses `AUTHARA_OAUTH_MICROSOFT_CLIENT_ID` and
327
+ `AUTHARA_OAUTH_MICROSOFT_TENANT_ID`; the browser obtains its authority and
328
+ one-time nonce from Core. Register the exact SPA redirect URI
329
+ `https://your-domain/auth/oauth/microsoft/redirect` in Microsoft Entra; Core
330
+ serves the MSAL redirect bridge at that path.
284
331
 
285
332
  By default, Vite proxies `/auth` to `http://localhost:3001`. Override the Core
286
333
  or gateway address when needed:
@@ -321,8 +368,8 @@ The Cloudflare route for `playground.authara.org` must use
321
368
  Compose network. Open `https://playground.authara.org`; `http://localhost:3000`
322
369
  remains available for direct local debugging.
323
370
 
324
- After configuring Google and Apple, fill their values in `.env` and set
325
- `AUTHARA_OAUTH_PROVIDERS=google,apple`. Until then, leave the provider list
371
+ After configuring the providers, fill their values in `.env` and set
372
+ `AUTHARA_OAUTH_PROVIDERS=google,apple,microsoft`. Until then, leave the provider list
326
373
  empty; the playground displays the disabled-provider fallbacks instead of
327
374
  attempting OAuth. Never commit `.env` or the tunnel token.
328
375
 
@@ -331,18 +378,21 @@ for this React contract; override `AUTHARA_CORE_VERSION` and
331
378
  `AUTHARA_MIGRATIONS_VERSION` only when deliberately testing another released
332
379
  pair.
333
380
 
334
- For the Compose deployment, configure both providers with the public origin
381
+ For the Compose deployment, configure all providers with the public origin
335
382
  `https://playground.authara.org`. The Google redirect URI is
336
383
  `https://playground.authara.org/auth/oauth/google/callback`; the Apple domain is
337
384
  `playground.authara.org` and its return URL is
338
385
  `https://playground.authara.org/auth/oauth/apple/callback`.
386
+ Register
387
+ `https://playground.authara.org/auth/oauth/microsoft/redirect` as the Microsoft
388
+ SPA redirect URI.
339
389
 
340
390
  The playground covers:
341
391
 
342
392
  - real login for `app`, `admin`, and `operator`
343
- - linking Google or Apple to the current account
344
- - signed-out Google-to-Apple and Apple-to-Google account-collision recovery
345
- - Google or Apple reauthentication with a challenge UUID
393
+ - linking Google, Apple, or Microsoft to the current account
394
+ - signed-out cross-provider account-collision recovery
395
+ - Google, Apple, or Microsoft reauthentication with a challenge UUID
346
396
  - text and icon variants in all three sizes
347
397
  - disabled-provider fallbacks, cancellation, and structured errors
348
398
  - current-session inspection without displaying access or refresh tokens
@@ -352,8 +402,8 @@ returns success without a challenge; an old session returns HTTP 428 and the
352
402
  playground fills the challenge UUID automatically.
353
403
 
354
404
  To rehearse account-collision recovery, use “Sign out,” select “Recover account
355
- collision,” and attempt normal Google or Apple login with an email already
356
- owned by the other provider. Authara's `409 account_link_required` response
405
+ collision,” and attempt a normal external-provider login with an email already
406
+ owned by another method. Authara's `409 account_link_required` response
357
407
  already contains the pending link, so the playground immediately renders the
358
408
  available existing-account proof buttons. It creates and consumes Core's real
359
409
  ten-minute `link_id`; it does not simulate either provider response.
@@ -373,10 +423,10 @@ error union deliberately requires human review so component mechanics and the
373
423
  documented application handling can be updated when necessary.
374
424
 
375
425
  Provider operations are discovered from Core's `get…LoginOptions` operations,
376
- so Microsoft error declarations will enter the generated unions automatically.
377
- A Microsoft button still needs its provider-SDK adapter and branded component;
378
- it can use the same `login`, `link`, `reauthenticate`, and `recover` action
379
- contract without a separate collision design.
426
+ so Microsoft error declarations enter the generated unions automatically. The
427
+ Microsoft client methods are optional locally so this package still accepts the
428
+ currently released Browser client and renders `fallback`; they become active
429
+ when Browser is regenerated from the Microsoft-enabled Core OpenAPI contract.
380
430
 
381
431
  ## License
382
432
 
@@ -0,0 +1,27 @@
1
+ import { type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from "react";
2
+ import { type AutharaActionProps, type MaybePromise } from "./contract.js";
3
+ import type { MicrosoftCredential } from "./services.js";
4
+ import { type AutharaColorMode } from "./color-mode.js";
5
+ export type MicrosoftButtonVariant = "icon" | "text";
6
+ export type MicrosoftButtonLabel = "continue" | "sign-in" | "sign-up";
7
+ export interface MicrosoftCredentialButtonProps extends Omit<ComponentPropsWithoutRef<"button">, "children" | "onError" | "size" | "type"> {
8
+ disabled?: boolean;
9
+ variant?: MicrosoftButtonVariant;
10
+ size?: "small" | "medium" | "large";
11
+ label?: MicrosoftButtonLabel;
12
+ buttonText?: string;
13
+ align?: "center" | "left";
14
+ mode?: AutharaColorMode;
15
+ border?: boolean;
16
+ width?: CSSProperties["width"];
17
+ fallback?: ReactNode;
18
+ onCredential: (credential: MicrosoftCredential) => MaybePromise<void>;
19
+ onCancel?: () => void;
20
+ onError?: (error: Error) => void;
21
+ onUnavailable?: () => void;
22
+ }
23
+ export declare function MicrosoftCredentialButton({ disabled, variant, size, label, buttonText, align, mode, border, width, fallback, onCredential, onCancel, onError, onUnavailable, style, onClick, ...buttonProps }: MicrosoftCredentialButtonProps): React.JSX.Element | null;
24
+ type MicrosoftButtonVisualProps = Omit<MicrosoftCredentialButtonProps, "onCredential" | "onError">;
25
+ export type MicrosoftButtonProps = MicrosoftButtonVisualProps & AutharaActionProps<"microsoft">;
26
+ export declare function MicrosoftButton(props: MicrosoftButtonProps): React.JSX.Element | null;
27
+ export {};
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, } from "react";
3
+ import { resolveAction, runMicrosoftAction } from "./actions.js";
4
+ import { useAutharaServices } from "./context.js";
5
+ import { isMicrosoftCancellation, isProviderUnavailable, toAutharaButtonError, toError, } from "./errors.js";
6
+ import { useResolvedAutharaColorMode, } from "./color-mode.js";
7
+ const textButtonHeights = { small: 30, medium: 36, large: 44 };
8
+ const iconButtonSizes = { small: 36, medium: 44, large: 52 };
9
+ function buttonLabel(label) {
10
+ switch (label) {
11
+ case "sign-in":
12
+ return "Sign in with Microsoft";
13
+ case "sign-up":
14
+ return "Sign up with Microsoft";
15
+ case "continue":
16
+ return "Continue with Microsoft";
17
+ }
18
+ }
19
+ function MicrosoftLogo() {
20
+ return (_jsxs("span", { "aria-hidden": "true", style: {
21
+ display: "grid",
22
+ width: 18,
23
+ height: 18,
24
+ flex: "0 0 18px",
25
+ gridTemplateColumns: "repeat(2, 1fr)",
26
+ gridTemplateRows: "repeat(2, 1fr)",
27
+ gap: 1,
28
+ }, children: [_jsx("span", { style: { background: "#f25022" } }), _jsx("span", { style: { background: "#7fba00" } }), _jsx("span", { style: { background: "#00a4ef" } }), _jsx("span", { style: { background: "#ffb900" } })] }));
29
+ }
30
+ export function MicrosoftCredentialButton({ disabled = false, variant = "text", size = "large", label = "continue", buttonText, align = "center", mode = "light", border = true, width, fallback = null, onCredential, onCancel, onError, onUnavailable, style, onClick, ...buttonProps }) {
31
+ const { microsoft } = useAutharaServices();
32
+ const credentialRef = useRef(onCredential);
33
+ const cancelRef = useRef(onCancel);
34
+ const errorRef = useRef(onError);
35
+ const unavailableRef = useRef(onUnavailable);
36
+ const [available, setAvailable] = useState(true);
37
+ const [ready, setReady] = useState(false);
38
+ const [busy, setBusy] = useState(false);
39
+ const resolvedMode = useResolvedAutharaColorMode(mode);
40
+ credentialRef.current = onCredential;
41
+ cancelRef.current = onCancel;
42
+ errorRef.current = onError;
43
+ unavailableRef.current = onUnavailable;
44
+ useEffect(() => {
45
+ let active = true;
46
+ void microsoft
47
+ .prepare()
48
+ .then(() => {
49
+ if (active)
50
+ setReady(true);
51
+ })
52
+ .catch((error) => {
53
+ if (!active)
54
+ return;
55
+ if (isProviderUnavailable(error)) {
56
+ setAvailable(false);
57
+ unavailableRef.current?.();
58
+ return;
59
+ }
60
+ errorRef.current?.(toError(error, "Microsoft authentication is unavailable."));
61
+ });
62
+ return () => {
63
+ active = false;
64
+ };
65
+ }, [microsoft]);
66
+ if (!available)
67
+ return _jsx(_Fragment, { children: fallback });
68
+ const iconOnly = variant === "icon";
69
+ const height = iconOnly ? iconButtonSizes[size] : textButtonHeights[size];
70
+ const text = buttonText ?? buttonLabel(label);
71
+ const inactive = disabled || busy;
72
+ const dark = resolvedMode === "dark";
73
+ return (_jsxs("button", { ...buttonProps, type: "button", disabled: !ready || inactive, "aria-busy": busy || undefined, "aria-label": iconOnly ? text : buttonProps["aria-label"], "data-authara-provider": "microsoft", "data-authara-ready": ready ? "true" : "false", onClick: (event) => {
74
+ onClick?.(event);
75
+ if (event.defaultPrevented || !ready || inactive)
76
+ return;
77
+ setReady(false);
78
+ setBusy(true);
79
+ const authorization = microsoft.authorize();
80
+ void authorization
81
+ .then((proof) => credentialRef.current(proof))
82
+ .catch((error) => {
83
+ if (isMicrosoftCancellation(error)) {
84
+ cancelRef.current?.();
85
+ return;
86
+ }
87
+ errorRef.current?.(toError(error, "Microsoft authentication failed."));
88
+ })
89
+ .finally(() => {
90
+ setBusy(false);
91
+ void microsoft
92
+ .prepare()
93
+ .then(() => setReady(true))
94
+ .catch((error) => {
95
+ errorRef.current?.(toError(error, "Microsoft authentication is unavailable."));
96
+ });
97
+ });
98
+ }, style: {
99
+ position: "relative",
100
+ display: "inline-flex",
101
+ width: width ?? (iconOnly ? height : "100%"),
102
+ height,
103
+ alignItems: "center",
104
+ justifyContent: "center",
105
+ gap: iconOnly || align === "left" ? 0 : 10,
106
+ padding: iconOnly ? 0 : "0 12px",
107
+ border: "1px solid",
108
+ borderColor: border ? (dark ? "#8e918f" : "#8c8c8c") : "transparent",
109
+ borderRadius: 12,
110
+ boxSizing: "border-box",
111
+ background: dark ? "#2f2f2f" : "#fff",
112
+ color: dark ? "#fff" : "#1f1f1f",
113
+ cursor: !ready ? "wait" : inactive ? "not-allowed" : "pointer",
114
+ fontFamily: '"Segoe UI", Arial, sans-serif',
115
+ fontSize: 14,
116
+ fontWeight: 600,
117
+ lineHeight: "20px",
118
+ opacity: inactive ? 0.62 : 1,
119
+ outlineOffset: 2,
120
+ transition: "opacity 150ms ease",
121
+ whiteSpace: "nowrap",
122
+ verticalAlign: "middle",
123
+ WebkitTapHighlightColor: "transparent",
124
+ ...style,
125
+ }, children: [_jsx("span", { style: {
126
+ display: "inline-flex",
127
+ position: !iconOnly && align === "left" ? "absolute" : "static",
128
+ left: !iconOnly && align === "left" ? 12 : undefined,
129
+ }, children: _jsx(MicrosoftLogo, {}) }), !iconOnly && _jsx("span", { children: text })] }));
130
+ }
131
+ export function MicrosoftButton(props) {
132
+ const { client } = useAutharaServices();
133
+ const internal = props;
134
+ const { action = "login", audience, authenticationChallengeId, recoveryLinkId, onSuccess, onError, label = action === "login" ? "sign-in" : "continue", ...buttonProps } = internal;
135
+ return (_jsx(MicrosoftCredentialButton, { ...buttonProps, label: label, onCredential: async (proof) => {
136
+ let selected;
137
+ try {
138
+ selected = resolveAction(action, audience, authenticationChallengeId, recoveryLinkId);
139
+ }
140
+ catch (error) {
141
+ throw toAutharaButtonError(error, "The Microsoft button action is not configured correctly.", "configuration");
142
+ }
143
+ let result;
144
+ try {
145
+ result = await runMicrosoftAction(client, selected, proof);
146
+ }
147
+ catch (error) {
148
+ throw toAutharaButtonError(error, `Microsoft ${action} failed.`, "authara");
149
+ }
150
+ try {
151
+ await onSuccess?.(result);
152
+ }
153
+ catch (error) {
154
+ throw toAutharaButtonError(error, "The Microsoft success callback failed.", "consumer");
155
+ }
156
+ }, onError: (error) => onError?.(toAutharaButtonError(error, "Microsoft authentication failed.", "provider")) }));
157
+ }
package/dist/actions.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AppleProof, AutharaAction, AutharaActionSuccess, AutharaAudience, AutharaReactClient, GoogleProof } from "./contract.js";
1
+ import type { AppleProof, AutharaAction, AutharaActionSuccess, AutharaAudience, AutharaReactClient, GoogleProof, MicrosoftProof } from "./contract.js";
2
2
  export type ResolvedAutharaAction = {
3
3
  action: "login";
4
4
  audience: AutharaAudience;
@@ -14,4 +14,5 @@ export type ResolvedAutharaAction = {
14
14
  };
15
15
  export declare function runGoogleAction(client: AutharaReactClient, selected: ResolvedAutharaAction, proof: GoogleProof): Promise<AutharaActionSuccess<"google">>;
16
16
  export declare function runAppleAction(client: AutharaReactClient, selected: ResolvedAutharaAction, proof: AppleProof): Promise<AutharaActionSuccess<"apple">>;
17
+ export declare function runMicrosoftAction(client: AutharaReactClient, selected: ResolvedAutharaAction, proof: MicrosoftProof): Promise<AutharaActionSuccess<"microsoft">>;
17
18
  export declare function resolveAction(action: AutharaAction | undefined, audience: AutharaAudience | undefined, authenticationChallengeId: string | undefined, recoveryLinkId: string | undefined): ResolvedAutharaAction;
package/dist/actions.js CHANGED
@@ -69,6 +69,55 @@ export async function runAppleAction(client, selected, proof) {
69
69
  };
70
70
  }
71
71
  }
72
+ export async function runMicrosoftAction(client, selected, proof) {
73
+ await client.getCsrfToken();
74
+ switch (selected.action) {
75
+ case "login":
76
+ if (!client.loginWithMicrosoft)
77
+ throw microsoftClientUnavailable();
78
+ return {
79
+ action: "login",
80
+ provider: "microsoft",
81
+ session: await client.loginWithMicrosoft({
82
+ audience: selected.audience,
83
+ body: proof,
84
+ }),
85
+ };
86
+ case "link":
87
+ if (!client.linkCurrentUserMicrosoft)
88
+ throw microsoftClientUnavailable();
89
+ await client.linkCurrentUserMicrosoft({ body: proof });
90
+ return { action: "link", provider: "microsoft" };
91
+ case "reauthenticate":
92
+ if (!client.reauthenticateWithMicrosoft)
93
+ throw microsoftClientUnavailable();
94
+ await client.reauthenticateWithMicrosoft({
95
+ body: {
96
+ ...proof,
97
+ authentication_challenge_id: selected.authenticationChallengeId,
98
+ },
99
+ });
100
+ return { action: "reauthenticate", provider: "microsoft" };
101
+ case "recover":
102
+ if (!client.completeAccountRecoveryLinkWithMicrosoft)
103
+ throw microsoftClientUnavailable();
104
+ return {
105
+ action: "recover",
106
+ provider: "microsoft",
107
+ session: await client.completeAccountRecoveryLinkWithMicrosoft({
108
+ linkID: selected.recoveryLinkId,
109
+ audience: selected.audience,
110
+ body: proof,
111
+ }),
112
+ };
113
+ }
114
+ }
115
+ function microsoftClientUnavailable() {
116
+ return {
117
+ status: 404,
118
+ message: "This @authara/browser client does not include Microsoft authentication.",
119
+ };
120
+ }
72
121
  export function resolveAction(action, audience, authenticationChallengeId, recoveryLinkId) {
73
122
  if (action === "link")
74
123
  return { action };
package/dist/context.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { type PropsWithChildren } from "react";
2
2
  import type { AutharaReactClient } from "./contract.js";
3
- import { AppleService, GoogleService } from "./services.js";
3
+ import { AppleService, GoogleService, MicrosoftService } from "./services.js";
4
4
  type AutharaReactContextValue = {
5
5
  apple: AppleService;
6
6
  client: AutharaReactClient;
7
7
  google: GoogleService;
8
+ microsoft: MicrosoftService;
8
9
  };
9
10
  export type AutharaProviderProps = PropsWithChildren<{
10
11
  client: AutharaReactClient;
11
12
  }>;
12
13
  /**
13
- * Owns the page-wide Google and Apple SDK configuration. Use one provider per
14
- * document so Google Identity Services is initialized only once.
14
+ * Owns the page-wide external-provider SDK configuration.
15
15
  */
16
16
  export declare function AutharaProvider({ client, children, }: AutharaProviderProps): React.JSX.Element;
17
17
  export declare function useAutharaServices(): AutharaReactContextValue;
package/dist/context.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo, } from "react";
3
- import { AppleService, GoogleService } from "./services.js";
3
+ import { AppleService, GoogleService, MicrosoftService } from "./services.js";
4
4
  const AutharaContext = createContext(null);
5
5
  /**
6
- * Owns the page-wide Google and Apple SDK configuration. Use one provider per
7
- * document so Google Identity Services is initialized only once.
6
+ * Owns the page-wide external-provider SDK configuration.
8
7
  */
9
8
  export function AutharaProvider({ client, children, }) {
10
9
  const value = useMemo(() => ({
11
10
  apple: new AppleService(client),
12
11
  client,
13
12
  google: new GoogleService(client),
13
+ microsoft: new MicrosoftService(client),
14
14
  }), [client]);
15
15
  return (_jsx(AutharaContext.Provider, { value: value, children: children }));
16
16
  }
@@ -2,9 +2,10 @@ import type { AccountRecoveryLink as BrowserAccountRecoveryLink, AuthSession } f
2
2
  import type { AutharaButtonError, AutharaLinkErrorCode, AutharaLoginErrorCode, AutharaRecoveryErrorCode, AutharaReauthenticationErrorCode } from "./errors.js";
3
3
  export type AutharaAudience = "app" | "admin" | "operator";
4
4
  export type AutharaAction = "login" | "link" | "reauthenticate" | "recover";
5
- export type AutharaProviderName = "apple" | "google";
6
- export type AutharaAccountRecoveryLink = BrowserAccountRecoveryLink & {
5
+ export type AutharaProviderName = "apple" | "google" | "microsoft";
6
+ export type AutharaAccountRecoveryLink = Omit<BrowserAccountRecoveryLink, "provider" | "proof_methods"> & {
7
7
  provider: AutharaProviderName;
8
+ proof_methods: Array<"password" | AutharaProviderName>;
8
9
  };
9
10
  export interface GoogleLoginOptions {
10
11
  client_id: string;
@@ -16,6 +17,11 @@ export interface AppleLoginOptions {
16
17
  state: string;
17
18
  nonce: string;
18
19
  }
20
+ export interface MicrosoftLoginOptions {
21
+ client_id: string;
22
+ authority: string;
23
+ nonce: string;
24
+ }
19
25
  export type GoogleProof = {
20
26
  code: string;
21
27
  redirect_uri: string;
@@ -24,10 +30,14 @@ export type AppleProof = {
24
30
  code: string;
25
31
  state: string;
26
32
  };
33
+ export type MicrosoftProof = {
34
+ credential: string;
35
+ nonce: string;
36
+ };
27
37
  /**
28
38
  * The generated @authara/browser contract slice used by the React components.
29
- * AutharaBrowserClient satisfies this interface once both providers are
30
- * available in the released Core contract.
39
+ * AutharaBrowserClient satisfies the required provider methods from the
40
+ * released Core contract. Microsoft remains optional during SDK rollout.
31
41
  */
32
42
  export interface AutharaReactClient {
33
43
  getCsrfToken(): Promise<unknown>;
@@ -67,6 +77,24 @@ export interface AutharaReactClient {
67
77
  audience?: "app";
68
78
  body: AppleProof;
69
79
  }): Promise<AuthSession>;
80
+ getMicrosoftLoginOptions?(): Promise<MicrosoftLoginOptions>;
81
+ loginWithMicrosoft?(options: {
82
+ audience?: AutharaAudience;
83
+ body: MicrosoftProof;
84
+ }): Promise<AuthSession>;
85
+ linkCurrentUserMicrosoft?(options: {
86
+ body: MicrosoftProof;
87
+ }): Promise<void>;
88
+ reauthenticateWithMicrosoft?(options: {
89
+ body: MicrosoftProof & {
90
+ authentication_challenge_id: string;
91
+ };
92
+ }): Promise<void>;
93
+ completeAccountRecoveryLinkWithMicrosoft?(options: {
94
+ linkID: AutharaAccountRecoveryLink["link_id"];
95
+ audience?: "app";
96
+ body: MicrosoftProof;
97
+ }): Promise<AuthSession>;
70
98
  }
71
99
  export type AutharaLoginSuccess<P extends AutharaProviderName> = {
72
100
  action: "login";
package/dist/errors.d.ts CHANGED
@@ -34,3 +34,4 @@ export declare class AutharaButtonError<Code extends string = AutharaButtonError
34
34
  export declare function toAutharaButtonError(error: unknown, fallback: string, defaultSource: AutharaButtonErrorSource): AutharaButtonError;
35
35
  export declare function isAutharaButtonError(error: unknown): error is AutharaButtonError;
36
36
  export declare function isAppleCancellation(error: unknown): boolean;
37
+ export declare function isMicrosoftCancellation(error: unknown): boolean;
package/dist/errors.js CHANGED
@@ -119,3 +119,11 @@ export function isAppleCancellation(error) {
119
119
  return (error.error === "user_cancelled_authorize" ||
120
120
  error.error === "popup_closed_by_user");
121
121
  }
122
+ export function isMicrosoftCancellation(error) {
123
+ if (typeof error !== "object" || error === null || !("errorCode" in error)) {
124
+ return false;
125
+ }
126
+ return (error.errorCode === "user_cancelled" ||
127
+ error.errorCode === "popup_window_error" ||
128
+ error.errorCode === "empty_window_error");
129
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { AppleButton, AppleCredentialButton, type AppleButtonLabel, type AppleButtonProps, type AppleButtonVariant, type AppleCredentialButtonProps, } from "./AppleButton.js";
2
2
  export { GoogleButton, GoogleCredentialButton, type GoogleButtonLabel, type GoogleButtonProps, type GoogleButtonVariant, type GoogleCredentialButtonProps, } from "./GoogleButton.js";
3
+ export { MicrosoftButton, MicrosoftCredentialButton, type MicrosoftButtonLabel, type MicrosoftButtonProps, type MicrosoftButtonVariant, type MicrosoftCredentialButtonProps, } from "./MicrosoftButton.js";
3
4
  export { AutharaProvider, type AutharaProviderProps } from "./context.js";
4
5
  export type { AutharaColorMode, ResolvedAutharaColorMode, } from "./color-mode.js";
5
6
  export { autharaActionErrorCodes, type AutharaContractAction, type AutharaContractErrorCode, } from "./generated-error-codes.js";
6
- export type { AppleLoginOptions, AppleProof, AutharaAction, AutharaAccountRecoveryLink, AutharaActionProps, AutharaActionSuccess, AutharaAudience, AutharaLinkSuccess, AutharaLoginSuccess, AutharaProviderName, AutharaRecoverySuccess, AutharaReauthenticationSuccess, AutharaReactClient, GoogleProof, GoogleLoginOptions, MaybePromise, } from "./contract.js";
7
- export { AutharaButtonError, isAppleCancellation, isAutharaButtonError, isProviderUnavailable, type AutharaAuthenticationChallenge, type AutharaButtonErrorCode, type AutharaButtonErrorSource, type AutharaLocalErrorCode, type AutharaLinkErrorCode, type AutharaLoginErrorCode, type AutharaRecoveryErrorCode, type AutharaReauthenticationErrorCode, } from "./errors.js";
8
- export type { AppleAuthorization, GoogleCredential } from "./services.js";
7
+ export type { AppleLoginOptions, AppleProof, AutharaAction, AutharaAccountRecoveryLink, AutharaActionProps, AutharaActionSuccess, AutharaAudience, AutharaLinkSuccess, AutharaLoginSuccess, AutharaProviderName, AutharaRecoverySuccess, AutharaReauthenticationSuccess, AutharaReactClient, GoogleProof, GoogleLoginOptions, MicrosoftLoginOptions, MicrosoftProof, MaybePromise, } from "./contract.js";
8
+ export { AutharaButtonError, isAppleCancellation, isMicrosoftCancellation, isAutharaButtonError, isProviderUnavailable, type AutharaAuthenticationChallenge, type AutharaButtonErrorCode, type AutharaButtonErrorSource, type AutharaLocalErrorCode, type AutharaLinkErrorCode, type AutharaLoginErrorCode, type AutharaRecoveryErrorCode, type AutharaReauthenticationErrorCode, } from "./errors.js";
9
+ export type { AppleAuthorization, GoogleCredential, MicrosoftCredential, } from "./services.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AppleButton, AppleCredentialButton, } from "./AppleButton.js";
2
2
  export { GoogleButton, GoogleCredentialButton, } from "./GoogleButton.js";
3
+ export { MicrosoftButton, MicrosoftCredentialButton, } from "./MicrosoftButton.js";
3
4
  export { AutharaProvider } from "./context.js";
4
5
  export { autharaActionErrorCodes, } from "./generated-error-codes.js";
5
- export { AutharaButtonError, isAppleCancellation, isAutharaButtonError, isProviderUnavailable, } from "./errors.js";
6
+ export { AutharaButtonError, isAppleCancellation, isMicrosoftCancellation, isAutharaButtonError, isProviderUnavailable, } from "./errors.js";
@@ -7,6 +7,10 @@ export type AppleAuthorization = {
7
7
  code: string;
8
8
  state: string;
9
9
  };
10
+ export type MicrosoftCredential = {
11
+ credential: string;
12
+ nonce: string;
13
+ };
10
14
  type GoogleHandler = (credential: GoogleCredential) => void;
11
15
  export declare class GoogleService {
12
16
  private readonly client;
@@ -27,4 +31,14 @@ export declare class AppleService {
27
31
  private renderOptions;
28
32
  private renderOfficialButton;
29
33
  }
34
+ export declare class MicrosoftService {
35
+ private readonly client;
36
+ private optionsPromise?;
37
+ private runtime?;
38
+ private runtimePromise?;
39
+ constructor(client: AutharaReactClient);
40
+ prepare(): Promise<void>;
41
+ authorize(): Promise<MicrosoftCredential>;
42
+ private options;
43
+ }
30
44
  export {};
package/dist/services.js CHANGED
@@ -102,3 +102,75 @@ export class AppleService {
102
102
  }
103
103
  }
104
104
  }
105
+ export class MicrosoftService {
106
+ constructor(client) {
107
+ this.client = client;
108
+ }
109
+ async prepare() {
110
+ if (this.runtime)
111
+ return;
112
+ this.runtimePromise ?? (this.runtimePromise = Promise.all([
113
+ this.options(),
114
+ import("@azure/msal-browser"),
115
+ ])
116
+ .then(async ([options, { PublicClientApplication }]) => {
117
+ const redirectUri = `${window.location.origin}/auth/oauth/microsoft/redirect`;
118
+ const application = new PublicClientApplication({
119
+ auth: {
120
+ clientId: options.client_id,
121
+ authority: options.authority,
122
+ redirectUri,
123
+ },
124
+ cache: { cacheLocation: "sessionStorage" },
125
+ });
126
+ await application.initialize();
127
+ this.runtime = { application, options, redirectUri };
128
+ return this.runtime;
129
+ })
130
+ .catch((error) => {
131
+ this.runtimePromise = undefined;
132
+ throw error;
133
+ }));
134
+ await this.runtimePromise;
135
+ }
136
+ authorize() {
137
+ const runtime = this.runtime;
138
+ if (!runtime) {
139
+ return Promise.reject(new Error("Microsoft authentication is still loading."));
140
+ }
141
+ this.runtime = undefined;
142
+ this.runtimePromise = undefined;
143
+ this.optionsPromise = undefined;
144
+ const resultPromise = runtime.application.loginPopup({
145
+ scopes: ["openid", "profile", "email"],
146
+ nonce: runtime.options.nonce,
147
+ prompt: "select_account",
148
+ redirectUri: runtime.redirectUri,
149
+ overrideInteractionInProgress: true,
150
+ });
151
+ return resultPromise.then((result) => {
152
+ if (!result.idToken) {
153
+ throw new Error("Microsoft did not return an ID token.");
154
+ }
155
+ return {
156
+ credential: result.idToken,
157
+ nonce: runtime.options.nonce,
158
+ };
159
+ });
160
+ }
161
+ options() {
162
+ if (!this.client.getMicrosoftLoginOptions) {
163
+ return Promise.reject({
164
+ status: 404,
165
+ message: "This @authara/browser client does not include Microsoft authentication.",
166
+ });
167
+ }
168
+ this.optionsPromise ?? (this.optionsPromise = this.client
169
+ .getMicrosoftLoginOptions()
170
+ .catch((error) => {
171
+ this.optionsPromise = undefined;
172
+ throw error;
173
+ }));
174
+ return this.optionsPromise;
175
+ }
176
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authara/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official React components for Authara browser authentication",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,11 +35,11 @@
35
35
  "prepublishOnly": "npm run check:contract && npm run build"
36
36
  },
37
37
  "peerDependencies": {
38
- "@authara/browser": ">=0.10.0 <0.11.0",
38
+ "@authara/browser": ">=0.11.0 <0.12.0",
39
39
  "react": "^18.0.0 || ^19.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@authara/browser": "0.10.0",
42
+ "@authara/browser": "0.11.0",
43
43
  "@eslint/js": "^9.39.2",
44
44
  "@testing-library/react": "^16.3.2",
45
45
  "@types/react": "^19.2.14",
@@ -56,5 +56,8 @@
56
56
  "vite": "^8.3.4",
57
57
  "vitest": "^4.0.18",
58
58
  "yaml": "2.9.0"
59
+ },
60
+ "dependencies": {
61
+ "@azure/msal-browser": "^5.25.0"
59
62
  }
60
63
  }