@owlmeans/client-auth 0.1.18-rc.4 → 0.1.18-rc.41
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 +280 -37
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-auth/SKILL.md +139 -21
- package/build/components/dispatcher/component.d.ts.map +1 -1
- package/build/components/dispatcher/component.js +14 -3
- package/build/components/dispatcher/component.js.map +1 -1
- package/build/entrypoints.d.ts +7 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +7 -0
- package/build/entrypoints.js.map +1 -0
- package/build/helper.d.ts +2 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +24 -3
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/login/adopt.d.ts +24 -0
- package/build/login/adopt.d.ts.map +1 -0
- package/build/login/adopt.js +28 -0
- package/build/login/adopt.js.map +1 -0
- package/build/login/consts.d.ts +49 -0
- package/build/login/consts.d.ts.map +1 -0
- package/build/login/consts.js +49 -0
- package/build/login/consts.js.map +1 -0
- package/build/login/credit.d.ts +24 -0
- package/build/login/credit.d.ts.map +1 -0
- package/build/login/credit.js +58 -0
- package/build/login/credit.js.map +1 -0
- package/build/login/env.d.ts +32 -0
- package/build/login/env.d.ts.map +1 -0
- package/build/login/env.js +84 -0
- package/build/login/env.js.map +1 -0
- package/build/login/flow.d.ts +20 -0
- package/build/login/flow.d.ts.map +1 -0
- package/build/login/flow.js +39 -0
- package/build/login/flow.js.map +1 -0
- package/build/login/hook.d.ts +37 -0
- package/build/login/hook.d.ts.map +1 -0
- package/build/login/hook.js +72 -0
- package/build/login/hook.js.map +1 -0
- package/build/login/i18n/be.json +37 -0
- package/build/login/i18n/de.json +37 -0
- package/build/login/i18n/en.json +37 -0
- package/build/login/i18n/es.json +37 -0
- package/build/login/i18n/fr.json +37 -0
- package/build/login/i18n/pl.json +37 -0
- package/build/login/i18n/ru.json +37 -0
- package/build/login/i18n/uk.json +37 -0
- package/build/login/i18n.d.ts +2 -0
- package/build/login/i18n.d.ts.map +1 -0
- package/build/login/i18n.js +27 -0
- package/build/login/i18n.js.map +1 -0
- package/build/login/index.d.ts +15 -0
- package/build/login/index.d.ts.map +1 -0
- package/build/login/index.js +15 -0
- package/build/login/index.js.map +1 -0
- package/build/login/methods.d.ts +25 -0
- package/build/login/methods.d.ts.map +1 -0
- package/build/login/methods.js +97 -0
- package/build/login/methods.js.map +1 -0
- package/build/login/resume.d.ts +30 -0
- package/build/login/resume.d.ts.map +1 -0
- package/build/login/resume.js +57 -0
- package/build/login/resume.js.map +1 -0
- package/build/login/screen.d.ts +17 -0
- package/build/login/screen.d.ts.map +1 -0
- package/build/login/screen.js +76 -0
- package/build/login/screen.js.map +1 -0
- package/build/login/service.d.ts +31 -0
- package/build/login/service.d.ts.map +1 -0
- package/build/login/service.js +148 -0
- package/build/login/service.js.map +1 -0
- package/build/login/surrogate.d.ts +20 -0
- package/build/login/surrogate.d.ts.map +1 -0
- package/build/login/surrogate.js +33 -0
- package/build/login/surrogate.js.map +1 -0
- package/build/login/terms.d.ts +20 -0
- package/build/login/terms.d.ts.map +1 -0
- package/build/login/terms.js +0 -0
- package/build/login/terms.js.map +1 -0
- package/build/login/types.d.ts +256 -0
- package/build/login/types.d.ts.map +1 -0
- package/build/login/types.js +31 -0
- package/build/login/types.js.map +1 -0
- package/build/manager/components/authentication/control.js +3 -3
- package/build/manager/components/authentication/control.js.map +1 -1
- package/build/manager/components/authentication/types.d.ts +2 -2
- package/build/manager/components/authentication/types.d.ts.map +1 -1
- package/build/manager/components/tunnel-consumer.js +2 -2
- package/build/manager/components/tunnel-consumer.js.map +1 -1
- package/build/manager/entrypoints.d.ts +11 -0
- package/build/manager/entrypoints.d.ts.map +1 -0
- package/build/manager/entrypoints.js +17 -0
- package/build/manager/entrypoints.js.map +1 -0
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.js +5 -0
- package/build/manager/plugins/basic-ed25519.js.map +1 -1
- package/build/manager/plugins/exports.d.ts +2 -0
- package/build/manager/plugins/exports.d.ts.map +1 -1
- package/build/manager/plugins/exports.js +2 -0
- package/build/manager/plugins/exports.js.map +1 -1
- package/build/manager/plugins/index.d.ts +1 -5
- package/build/manager/plugins/index.d.ts.map +1 -1
- package/build/manager/plugins/index.js +9 -1
- package/build/manager/plugins/index.js.map +1 -1
- package/build/manager/plugins/methods.d.ts +6 -0
- package/build/manager/plugins/methods.d.ts.map +1 -0
- package/build/manager/plugins/methods.js +65 -0
- package/build/manager/plugins/methods.js.map +1 -0
- package/build/manager/plugins/re-captcha.d.ts.map +1 -1
- package/build/manager/plugins/re-captcha.js +5 -0
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/registry.d.ts +20 -0
- package/build/manager/plugins/registry.d.ts.map +1 -0
- package/build/manager/plugins/registry.js +19 -0
- package/build/manager/plugins/registry.js.map +1 -0
- package/build/manager/plugins/tunnel-consumer.d.ts.map +1 -1
- package/build/manager/plugins/tunnel-consumer.js +8 -1
- package/build/manager/plugins/tunnel-consumer.js.map +1 -1
- package/build/manager/plugins/types.d.ts +53 -0
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/build/service.js +1 -1
- package/build/service.js.map +1 -1
- package/package.json +36 -27
- package/src/components/dispatcher/component.tsx +16 -3
- package/src/entrypoints.ts +10 -0
- package/src/helper.ts +26 -3
- package/src/index.ts +1 -1
- package/src/login/adopt.ts +31 -0
- package/src/login/consts.ts +64 -0
- package/src/login/credit.ts +75 -0
- package/src/login/env.ts +86 -0
- package/src/login/flow.ts +44 -0
- package/src/login/hook.ts +80 -0
- package/src/login/i18n/be.json +37 -0
- package/src/login/i18n/de.json +37 -0
- package/src/login/i18n/en.json +37 -0
- package/src/login/i18n/es.json +37 -0
- package/src/login/i18n/fr.json +37 -0
- package/src/login/i18n/pl.json +37 -0
- package/src/login/i18n/ru.json +37 -0
- package/src/login/i18n/uk.json +37 -0
- package/src/login/i18n.ts +28 -0
- package/src/login/index.ts +15 -0
- package/src/login/methods.ts +113 -0
- package/src/login/resume.ts +58 -0
- package/src/login/screen.tsx +136 -0
- package/src/login/service.ts +195 -0
- package/src/login/surrogate.ts +45 -0
- package/src/login/terms.ts +0 -0
- package/src/login/types.ts +279 -0
- package/src/manager/README.md +37 -80
- package/src/manager/components/authentication/control.ts +3 -3
- package/src/manager/components/authentication/types.ts +2 -2
- package/src/manager/components/tunnel-consumer.tsx +2 -2
- package/src/manager/entrypoints.ts +18 -0
- package/src/manager/plugins/basic-ed25519.tsx +7 -0
- package/src/manager/plugins/exports.ts +2 -0
- package/src/manager/plugins/index.ts +10 -4
- package/src/manager/plugins/methods.ts +74 -0
- package/src/manager/plugins/re-captcha.tsx +7 -0
- package/src/manager/plugins/registry.ts +26 -0
- package/src/manager/plugins/tunnel-consumer.tsx +10 -1
- package/src/manager/plugins/types.ts +54 -0
- package/src/service.ts +1 -1
- package/tests/context.ts +1 -2
- package/tests/login.spec.ts +332 -0
- package/build/.gitkeep +0 -0
- package/build/manager/modules.d.ts +0 -2
- package/build/manager/modules.d.ts.map +0 -1
- package/build/manager/modules.js +0 -16
- package/build/manager/modules.js.map +0 -1
- package/build/modules.d.ts +0 -4
- package/build/modules.d.ts.map +0 -1
- package/build/modules.js +0 -9
- package/build/modules.js.map +0 -1
- package/src/manager/modules.ts +0 -18
- package/src/modules.ts +0 -13
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { ComponentType, CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import type { LazyService, BasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { LoginMethodEmphasis, LoginScreenConfig, LoginTermsConfig } from '@owlmeans/config'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The context a login plugin is handed.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately `BasicContext<any>`, matching `RouterService`: a plugin only ever reaches services
|
|
9
|
+
* by alias, and pinning the config parameter would make every concrete app config — each of which
|
|
10
|
+
* adds its own required fields — fail to satisfy it, since `makeContext` is invariant in it.
|
|
11
|
+
*/
|
|
12
|
+
export type LoginContext = BasicContext<any>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Environment descriptor the cascade selects a login plugin on.
|
|
16
|
+
*
|
|
17
|
+
* This is the single source of environment truth — do NOT bake `window.self !== window.top` or a
|
|
18
|
+
* `sessionStorage` probe into a plugin's `match`. A native host will supply its own descriptor
|
|
19
|
+
* (`hasWindow: false`) so an in-app-browser plugin wins there without any DOM check.
|
|
20
|
+
*/
|
|
21
|
+
export interface LoginEnv {
|
|
22
|
+
/** A DOM is present at all (false under SSR, native, and unit tests). */
|
|
23
|
+
hasWindow: boolean
|
|
24
|
+
/** This document is embedded in a frame. A cross-origin `top` read throws — that throw counts. */
|
|
25
|
+
embedded: boolean
|
|
26
|
+
/** This document IS the surrogate login window; survives the cross-origin round trip. */
|
|
27
|
+
surrogate: boolean
|
|
28
|
+
/** The window that opened this one is still reachable (COOP severs it permanently). */
|
|
29
|
+
hasOpener: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What a stage did, and therefore what the caller must do next. */
|
|
33
|
+
export enum LoginOutcome {
|
|
34
|
+
/** The plugin took the flow over and it is finished. The caller does nothing more. */
|
|
35
|
+
Handled = 'handled',
|
|
36
|
+
/** The plugin did nothing — the caller carries on with its ordinary continuation. */
|
|
37
|
+
Passed = 'passed',
|
|
38
|
+
/** The browser is leaving this document. The caller must not navigate or render. */
|
|
39
|
+
Redirected = 'redirected',
|
|
40
|
+
/** Cannot proceed without a fresh user gesture — the caller renders a sign-in control. */
|
|
41
|
+
Gesture = 'gesture',
|
|
42
|
+
/** Authenticated, but with no channel back to the window that started it. */
|
|
43
|
+
Orphaned = 'orphaned',
|
|
44
|
+
/** The attempt ended with no token (the user closed the window, or the provider refused it). */
|
|
45
|
+
Failed = 'failed',
|
|
46
|
+
/**
|
|
47
|
+
* A window this flow needed could not be opened at all — `window.open` returned `null`, which is
|
|
48
|
+
* the browser's own popup blocker, not a flow failure. Distinct from {@link Failed}: a caller
|
|
49
|
+
* with nowhere inline to render (a header "Log in"/"Log out" control, not the sign-in screen)
|
|
50
|
+
* needs to know specifically that a fresh click reopening the SAME control will not help, and
|
|
51
|
+
* that the browser is already showing its own blocked-popup affordance somewhere.
|
|
52
|
+
*/
|
|
53
|
+
Blocked = 'blocked',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Why a surrogate window was opened. */
|
|
57
|
+
export enum LoginIntent {
|
|
58
|
+
Login = 'login',
|
|
59
|
+
Logout = 'logout',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoginRequest {
|
|
63
|
+
/** Where the login flow starts — a resolved dispatcher path, or the current address. */
|
|
64
|
+
url: string
|
|
65
|
+
/**
|
|
66
|
+
* The ordinary in-app continuation, supplied by the caller because only a component may call
|
|
67
|
+
* `useNavigate`. A plugin that decides not to take the flow over calls this; without it the
|
|
68
|
+
* fallback is a full page load of {@link url}.
|
|
69
|
+
*/
|
|
70
|
+
navigate?: () => void | Promise<void>
|
|
71
|
+
/** Entrypoint alias to return to after login. */
|
|
72
|
+
target?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LogoutRequest {
|
|
76
|
+
/** Where a surrogate logout runs — a resolved surrogate path. */
|
|
77
|
+
url: string
|
|
78
|
+
/** The in-app continuation once the local session is gone. */
|
|
79
|
+
navigate?: () => void | Promise<void>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A pluggable login mechanic — WHERE the authorization round trip runs.
|
|
84
|
+
*
|
|
85
|
+
* Not to be confused with `AuthenticationPlugin` (./manager/plugins), which answers *how* a user
|
|
86
|
+
* proves their identity. This one answers *in which browsing context the flow can complete*: a
|
|
87
|
+
* redirect works in an ordinary tab and cannot work inside a frame whose provider refuses to be
|
|
88
|
+
* embedded, and that difference is a property of the environment, not of the credential.
|
|
89
|
+
*/
|
|
90
|
+
export interface LoginPlugin {
|
|
91
|
+
alias: string
|
|
92
|
+
/** Higher wins among matching plugins. Defaults to 0. */
|
|
93
|
+
priority?: number
|
|
94
|
+
/** Free-form tag, e.g. 'redirect' | 'surrogate' | 'native'. */
|
|
95
|
+
mode?: string
|
|
96
|
+
/** Selector; `undefined` means "always applicable". */
|
|
97
|
+
match?: (env: LoginEnv, ctx?: LoginContext) => boolean
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Record, while it is still knowable, whatever the later stages need about this document.
|
|
101
|
+
*
|
|
102
|
+
* Called synchronously as the FIRST statement of the dispatcher's effect: everything after it
|
|
103
|
+
* can navigate away, and some of the evidence (notably `window.name`) is cleared by the browser
|
|
104
|
+
* the moment a top-level context goes cross-origin.
|
|
105
|
+
*/
|
|
106
|
+
enter?: (ctx: LoginContext, env: LoginEnv) => void
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Start login from a user gesture.
|
|
110
|
+
*
|
|
111
|
+
* MUST open any window synchronously — the popup blocker only yields while the gesture is still
|
|
112
|
+
* being handled. Implement as a NON-async function returning a promise.
|
|
113
|
+
*/
|
|
114
|
+
begin: (ctx: LoginContext, request: LoginRequest, env: LoginEnv) => Promise<LoginOutcome>
|
|
115
|
+
|
|
116
|
+
/** Send the browser to the identity provider's authorization URL. */
|
|
117
|
+
authorize: (ctx: LoginContext, url: string, env: LoginEnv) => Promise<LoginOutcome>
|
|
118
|
+
|
|
119
|
+
/** A bearer token was issued in this document — decide where it goes. */
|
|
120
|
+
complete: (ctx: LoginContext, token: string, env: LoginEnv) => Promise<LoginOutcome>
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* This document ALREADY holds a session — decide whether it is useful here.
|
|
124
|
+
*
|
|
125
|
+
* Absent means `Passed`: keep it and carry on, which is what an ordinary tab has always done.
|
|
126
|
+
* A surrogate hands it back to its opener instead, which is the whole point: a popup that
|
|
127
|
+
* discovers an existing session must sign the framed application in, not display the
|
|
128
|
+
* application to itself.
|
|
129
|
+
*
|
|
130
|
+
* Deliberately not `complete`: "a token was just issued here" and "a token was already here"
|
|
131
|
+
* are different facts, and a silent-refresh plugin will need to tell them apart.
|
|
132
|
+
*/
|
|
133
|
+
resume?: (ctx: LoginContext, token: string, env: LoginEnv) => Promise<LoginOutcome>
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Start logout from a user gesture.
|
|
137
|
+
*
|
|
138
|
+
* MUST open any window synchronously, for exactly the reason `begin` must — logging out of a
|
|
139
|
+
* framed application opens a window too. Implement as a NON-async function.
|
|
140
|
+
*/
|
|
141
|
+
logout?: (ctx: LoginContext, request: LogoutRequest, env: LoginEnv) => Promise<LoginOutcome>
|
|
142
|
+
|
|
143
|
+
/** The local session is gone in this document — decide what to tell whom. */
|
|
144
|
+
logoutComplete?: (ctx: LoginContext, env: LoginEnv) => Promise<LoginOutcome>
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Something that must be true before a login flow may start.
|
|
149
|
+
*
|
|
150
|
+
* SYNCHRONOUS on purpose. `begin` must not cross a microtask boundary before a plugin's
|
|
151
|
+
* `window.open`, or the popup blocker eats the window. A precondition that has to ask a server
|
|
152
|
+
* belongs somewhere else entirely.
|
|
153
|
+
*
|
|
154
|
+
* Returning false stops the flow and resolves `begin` as {@link LoginOutcome.Gesture} — which
|
|
155
|
+
* already means "cannot proceed without a fresh user gesture; render a control", and is exactly
|
|
156
|
+
* the state a user is in after a blocking dialog has opened over the page.
|
|
157
|
+
*/
|
|
158
|
+
export interface LoginPrecondition {
|
|
159
|
+
alias: string
|
|
160
|
+
/** Higher runs first. Defaults to 0. */
|
|
161
|
+
priority?: number
|
|
162
|
+
check: (ctx: LoginContext, request: LoginRequest, env: LoginEnv) => boolean
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** What a source needs in order to describe the methods it offers. */
|
|
166
|
+
export interface LoginMethodContext {
|
|
167
|
+
context: LoginContext
|
|
168
|
+
env: LoginEnv
|
|
169
|
+
/** In-app navigation, when a component supplied it. */
|
|
170
|
+
navigate?: (alias: string, params?: Record<string, string>) => void | Promise<void>
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* One way to sign in, as the screen renders it.
|
|
175
|
+
*
|
|
176
|
+
* `start` is what a button calls. It MUST be callable synchronously from a click — branch on
|
|
177
|
+
* `env.embedded && !env.surrogate` first and open any window before the first `await`.
|
|
178
|
+
*/
|
|
179
|
+
export interface LoginMethod {
|
|
180
|
+
id: string
|
|
181
|
+
/** Set when an `AuthenticationPlugin` drives this method. */
|
|
182
|
+
type?: string
|
|
183
|
+
label?: string
|
|
184
|
+
i18nKey?: string
|
|
185
|
+
icon?: string
|
|
186
|
+
order?: number
|
|
187
|
+
emphasis?: LoginMethodEmphasis
|
|
188
|
+
restricted?: boolean
|
|
189
|
+
params?: Record<string, string>
|
|
190
|
+
start: (ctx: LoginMethodContext) => Promise<LoginOutcome>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface LoginMethodSource {
|
|
194
|
+
alias: string
|
|
195
|
+
list: (ctx: LoginMethodContext) => LoginMethod[]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** What every rendering of the sign-in screen accepts, whatever its UI family. */
|
|
199
|
+
export interface LoginScreenProps {
|
|
200
|
+
/** The one thing a consuming application is expected to supply. */
|
|
201
|
+
Logo?: ComponentType<{ className?: string }> | ReactNode
|
|
202
|
+
title?: ReactNode
|
|
203
|
+
subtitle?: ReactNode
|
|
204
|
+
/**
|
|
205
|
+
* `(key, defaultValue) => string`. A prop, never an implicit context read: a component that
|
|
206
|
+
* reaches for an i18n provider crashes the whole render in an app mounted without one.
|
|
207
|
+
*/
|
|
208
|
+
translate?: (key: string, defaultValue: string) => string
|
|
209
|
+
/** Replace or reorder what the resolver produced. */
|
|
210
|
+
methods?: LoginMethod[] | ((methods: LoginMethod[]) => LoginMethod[])
|
|
211
|
+
terms?: LoginTermsConfig | false
|
|
212
|
+
config?: LoginScreenConfig
|
|
213
|
+
/** Replaces the composed credit line entirely. */
|
|
214
|
+
footer?: ReactNode
|
|
215
|
+
className?: string
|
|
216
|
+
containerClassName?: string
|
|
217
|
+
/**
|
|
218
|
+
* Inline overrides for the screen's outer box.
|
|
219
|
+
*
|
|
220
|
+
* It exists because the outer box carries its viewport height inline rather than as a utility
|
|
221
|
+
* class, and an escape hatch that a class can no longer provide has to be provided some other
|
|
222
|
+
* way. See the note on the screen itself.
|
|
223
|
+
*/
|
|
224
|
+
style?: CSSProperties
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type LoginScreenComponent = ComponentType<LoginScreenProps>
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A way to surface a `begin`/`logout` outcome that has no inline screen to render it on.
|
|
231
|
+
*
|
|
232
|
+
* A header "Log in"/"Log out" control (`useLogin`/`useLogout`) fires the facade and forgets the
|
|
233
|
+
* result — it renders nothing of its own, unlike the sign-in screen, which already shows
|
|
234
|
+
* `loginAttemptError` inline. Registering one is how a UI package (`web-panel`, `mui-panel`) gives
|
|
235
|
+
* that control a way to speak — e.g. a toast when {@link LoginOutcome.Blocked} fires. Unregistered,
|
|
236
|
+
* it is silence, which is what a non-DOM host and a screen-mounted flow both already have.
|
|
237
|
+
*/
|
|
238
|
+
export type LoginNotifier = (outcome: LoginOutcome, env: LoginEnv) => void
|
|
239
|
+
|
|
240
|
+
export interface LoginService extends LazyService {
|
|
241
|
+
registerPlugin: (plugin: LoginPlugin) => void
|
|
242
|
+
/** Select the active plugin for the given (or default) environment. */
|
|
243
|
+
plugin: (env?: LoginEnv) => LoginPlugin
|
|
244
|
+
/** The environment the cascade is currently selecting on. */
|
|
245
|
+
env: () => LoginEnv
|
|
246
|
+
/** Something that must hold before any flow starts. Checked synchronously, in `begin`. */
|
|
247
|
+
registerPrecondition: (precondition: LoginPrecondition) => void
|
|
248
|
+
/** A source of offerable sign-in methods, scoped to this context. */
|
|
249
|
+
registerMethodSource: (source: LoginMethodSource) => void
|
|
250
|
+
methods: (ctx: LoginMethodContext) => LoginMethod[]
|
|
251
|
+
/**
|
|
252
|
+
* The screen a dispatcher renders when it cannot proceed.
|
|
253
|
+
*
|
|
254
|
+
* A slot rather than an import, because a relying party (`web-oidc-rp`) must never depend on a
|
|
255
|
+
* UI family (`web-panel` / `mui-panel`) — that edge would force every relying party to pick one.
|
|
256
|
+
*/
|
|
257
|
+
registerScreen: (screen: LoginScreenComponent) => void
|
|
258
|
+
screen: () => LoginScreenComponent | null
|
|
259
|
+
/** See {@link LoginNotifier}. Replaces any previously registered notifier. */
|
|
260
|
+
registerNotifier: (notifier: LoginNotifier) => void
|
|
261
|
+
// Facade — every method re-selects the plugin and delegates.
|
|
262
|
+
// NOTE: these stay plain writable instance properties, never getters, so that alternative
|
|
263
|
+
// implementations (e.g. a native login service) can monkey-patch them directly.
|
|
264
|
+
enter: () => void
|
|
265
|
+
begin: (request: LoginRequest) => Promise<LoginOutcome>
|
|
266
|
+
authorize: (url: string) => Promise<LoginOutcome>
|
|
267
|
+
complete: (token: string) => Promise<LoginOutcome>
|
|
268
|
+
resume: (token: string) => Promise<LoginOutcome>
|
|
269
|
+
logout: (request: LogoutRequest) => Promise<LoginOutcome>
|
|
270
|
+
logoutComplete: () => Promise<LoginOutcome>
|
|
271
|
+
/** Adopt an issued bearer token as this context's authentication. */
|
|
272
|
+
adopt: (token: string) => Promise<void>
|
|
273
|
+
/** Drop this document's authentication. The single de-adoption path. */
|
|
274
|
+
revoke: () => Promise<void>
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface LoginServiceAppend {
|
|
278
|
+
login: () => LoginService
|
|
279
|
+
}
|
package/src/manager/README.md
CHANGED
|
@@ -21,9 +21,9 @@ React components that handle various authentication scenarios:
|
|
|
21
21
|
- **Tunnel Consumer**: Component for handling WebSocket authentication tunnels
|
|
22
22
|
- **Credential Management**: Secure handling of user credentials
|
|
23
23
|
|
|
24
|
-
###
|
|
25
|
-
The manager integrates with the OwlMeans
|
|
26
|
-
- **
|
|
24
|
+
### Entrypoint Integration
|
|
25
|
+
The manager integrates with the OwlMeans entrypoint system to provide:
|
|
26
|
+
- **Enhanced Entrypoints**: Entrypoints with client-specific authentication handling
|
|
27
27
|
- **Route Protection**: Automatic authentication requirements for protected routes
|
|
28
28
|
- **API Integration**: Seamless integration with authentication APIs
|
|
29
29
|
|
|
@@ -37,7 +37,7 @@ Comprehensive error handling for authentication scenarios:
|
|
|
37
37
|
This manager is part of the `@owlmeans/client-auth` package:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
npm install @owlmeans/client-auth
|
|
40
|
+
npm install @owlmeans/client-auth@^0.1.18-rc.41
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## API Reference
|
|
@@ -134,37 +134,22 @@ try {
|
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
###
|
|
137
|
+
### Entrypoints
|
|
138
138
|
|
|
139
|
-
#### `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
const modules: CommonModule[]
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Contains elevated modules for:
|
|
147
|
-
- `AUTHEN` - Base authentication module
|
|
148
|
-
- `AUTHEN_INIT` - Authentication initialization
|
|
149
|
-
- `AUTHEN_AUTHEN` - User authentication
|
|
150
|
-
- `AUTHEN_RELY` - Authentication relay
|
|
151
|
-
- `CAUTHEN` - Client authentication
|
|
152
|
-
- `CAUTHEN_AUTHEN` - Client authentication handler
|
|
153
|
-
- `CAUTHEN_AUTHEN_DEFAULT` - Default authentication component
|
|
154
|
-
- `CAUTHEN_AUTHEN_TYPED` - Typed authentication component
|
|
155
|
-
- `DISPATCHER` - Authentication dispatcher
|
|
139
|
+
#### `entrypoints`
|
|
140
|
+
The shared, immutable authentication declarations. Bind them into the client context alongside
|
|
141
|
+
the application's own entrypoints.
|
|
156
142
|
|
|
157
143
|
## Usage Examples
|
|
158
144
|
|
|
159
145
|
### Basic Authentication Setup
|
|
160
146
|
|
|
161
147
|
```typescript
|
|
162
|
-
import {
|
|
163
|
-
import {
|
|
148
|
+
import { entrypoints } from '@owlmeans/client-auth/manager'
|
|
149
|
+
import { makeContext } from '@owlmeans/web-client'
|
|
164
150
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
context.registerModules(modules)
|
|
151
|
+
const context = makeContext(config)
|
|
152
|
+
context.registerEntrypoints(entrypoints)
|
|
168
153
|
|
|
169
154
|
await context.configure().init()
|
|
170
155
|
```
|
|
@@ -256,48 +241,27 @@ function RealtimeApp() {
|
|
|
256
241
|
|
|
257
242
|
### Custom Authentication Flow
|
|
258
243
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
import { useModule } from '@owlmeans/client'
|
|
244
|
+
Use the typed authentication protocols from `@owlmeans/auth-common`; the context supplies the
|
|
245
|
+
registered client binding and infers the request and response types from the protocol.
|
|
262
246
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
console.error('Invalid credentials:', error.message)
|
|
281
|
-
} else {
|
|
282
|
-
console.error('Authentication failed:', error)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
247
|
+
```typescript
|
|
248
|
+
import { AuthenticationType } from '@owlmeans/auth'
|
|
249
|
+
import { authProtocols } from '@owlmeans/auth-common'
|
|
250
|
+
import { AuthenCredError } from '@owlmeans/client-auth/manager'
|
|
251
|
+
import type { ClientContext } from '@owlmeans/client-context'
|
|
252
|
+
|
|
253
|
+
async function authenticate(context: ClientContext, credentials: { username: string, password: string }) {
|
|
254
|
+
try {
|
|
255
|
+
const allowance = await context.entrypoint(authProtocols.init).call({
|
|
256
|
+
body: { type: AuthenticationType.PasswordLogin },
|
|
257
|
+
})
|
|
258
|
+
return await context.entrypoint(authProtocols.authenticate).call({
|
|
259
|
+
body: { ...credentials, challenge: allowance.challenge },
|
|
260
|
+
})
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof AuthenCredError) console.error('Invalid credentials:', error.message)
|
|
263
|
+
throw error
|
|
285
264
|
}
|
|
286
|
-
|
|
287
|
-
return (
|
|
288
|
-
<form onSubmit={(e) => {
|
|
289
|
-
e.preventDefault()
|
|
290
|
-
const formData = new FormData(e.target)
|
|
291
|
-
handleLogin({
|
|
292
|
-
username: formData.get('username'),
|
|
293
|
-
password: formData.get('password')
|
|
294
|
-
})
|
|
295
|
-
}}>
|
|
296
|
-
<input name="username" placeholder="Username" required />
|
|
297
|
-
<input name="password" type="password" placeholder="Password" required />
|
|
298
|
-
<button type="submit">Login</button>
|
|
299
|
-
</form>
|
|
300
|
-
)
|
|
301
265
|
}
|
|
302
266
|
```
|
|
303
267
|
|
|
@@ -408,18 +372,11 @@ Single-use token authentication for enhanced security.
|
|
|
408
372
|
5. **Graceful Fallbacks**: Provide fallback UI for authentication failures
|
|
409
373
|
6. **Security**: Never store sensitive credentials in local storage
|
|
410
374
|
|
|
411
|
-
##
|
|
412
|
-
|
|
413
|
-
The manager uses module elevation to enhance base authentication modules:
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
416
|
-
// Base modules are elevated with client-specific handlers
|
|
417
|
-
elevate(list, AUTHEN_INIT, true) // API call elevation
|
|
418
|
-
elevate(list, AUTHEN_AUTHEN, true) // Authentication elevation
|
|
419
|
-
elevate(list, CAUTHEN_AUTHEN_DEFAULT, handler(AuthenticationHOC())) // Component handler
|
|
420
|
-
```
|
|
375
|
+
## Entrypoint wiring
|
|
421
376
|
|
|
422
|
-
|
|
377
|
+
The manager exports a ready-to-use `entrypoints` collection. Compose it with the application's
|
|
378
|
+
own declarations and use `bind`/`bindScreen` from `@owlmeans/client-entrypoint` when adding a
|
|
379
|
+
custom client handler; handlers are attached to declaration objects, never looked up by alias.
|
|
423
380
|
|
|
424
381
|
## Dependencies
|
|
425
382
|
|
|
@@ -427,7 +384,7 @@ This manager depends on:
|
|
|
427
384
|
- `@owlmeans/auth` - Core authentication functionality
|
|
428
385
|
- `@owlmeans/auth-common` - Shared authentication components
|
|
429
386
|
- `@owlmeans/client` - Client-side framework functionality
|
|
430
|
-
- `@owlmeans/client-
|
|
387
|
+
- `@owlmeans/client-entrypoint` - Typed protocol binding system
|
|
431
388
|
- `@owlmeans/error` - Error management system
|
|
432
389
|
- `react` - React framework for UI components
|
|
433
390
|
|
|
@@ -436,4 +393,4 @@ This manager depends on:
|
|
|
436
393
|
- [`@owlmeans/client-auth`](../../../README.md) - Parent client authentication package
|
|
437
394
|
- [`@owlmeans/server-auth/manager`](../../../server-auth/src/manager/README.md) - Server-side authentication manager
|
|
438
395
|
- [`@owlmeans/auth`](../../../auth/README.md) - Core authentication library
|
|
439
|
-
- [`@owlmeans/client`](../../../client/README.md) - Client-side framework
|
|
396
|
+
- [`@owlmeans/client`](../../../client/README.md) - Client-side framework
|
|
@@ -45,7 +45,7 @@ export const makeControl = (
|
|
|
45
45
|
control.afterAuthenticate = plugins[control.type].afterAuthenticate
|
|
46
46
|
|
|
47
47
|
const module = context.entrypoint<ClientEntrypoint<AllowanceResponse>>(AUTHEN_INIT)
|
|
48
|
-
const
|
|
48
|
+
const allowance = await module.call({ body: control.request })
|
|
49
49
|
|
|
50
50
|
control.allowance = allowance
|
|
51
51
|
|
|
@@ -89,8 +89,8 @@ export const makeControl = (
|
|
|
89
89
|
// We return back unwrapped challenge
|
|
90
90
|
credentials.challenge = control.allowance?.challenge
|
|
91
91
|
|
|
92
|
-
const
|
|
93
|
-
.
|
|
92
|
+
const { value: token, outcome: status } = await context.entrypoint<ClientEntrypoint<AuthToken>>(AUTHEN_AUTHEN)
|
|
93
|
+
.invoke({ body: credentials })
|
|
94
94
|
|
|
95
95
|
if (status === EntrypointOutcome.Ok && token.token != null
|
|
96
96
|
&& token.token !== '' && control.afterAuthenticate != null) {
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
AllowanceRequest, AllowanceResponse, AuthCredentials, AuthenticationStage, AuthenticationType,
|
|
3
3
|
AuthToken
|
|
4
4
|
} from '@owlmeans/auth'
|
|
5
|
-
import type {
|
|
5
|
+
import type { EntrypointContextParams } from '@owlmeans/client'
|
|
6
6
|
import type { FC } from 'react'
|
|
7
7
|
import type { ClientContext } from '@owlmeans/client'
|
|
8
8
|
import type { FlowService } from '@owlmeans/client-flow'
|
|
@@ -10,7 +10,7 @@ import type { ResilientError } from '@owlmeans/error'
|
|
|
10
10
|
|
|
11
11
|
export type ClientAuthType = AuthenticationType | string
|
|
12
12
|
|
|
13
|
-
export interface AuthenticationProps extends
|
|
13
|
+
export interface AuthenticationProps extends EntrypointContextParams {
|
|
14
14
|
type?: ClientAuthType
|
|
15
15
|
callback?: AuthenticationCallback
|
|
16
16
|
source?: string
|
|
@@ -2,11 +2,11 @@ import { AuthenticationType } from '@owlmeans/auth'
|
|
|
2
2
|
import { AuthenticationHOC } from './authentication/component.js'
|
|
3
3
|
import type { FC } from 'react'
|
|
4
4
|
import { useMemo } from 'react'
|
|
5
|
-
import {
|
|
5
|
+
import { useEntrypoint } from '@owlmeans/client'
|
|
6
6
|
import type { TunnelAuthenticationProps } from './types.js'
|
|
7
7
|
|
|
8
8
|
export const TunnelConsumer: FC<Partial<TunnelAuthenticationProps>> = ({ ...props }) => {
|
|
9
|
-
const mod =
|
|
9
|
+
const mod = useEntrypoint()
|
|
10
10
|
const Implementation = useMemo(
|
|
11
11
|
() => AuthenticationHOC(undefined, AuthenticationType.WalletConsumer), []
|
|
12
12
|
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { authProtocols } from '@owlmeans/auth-common'
|
|
3
|
+
import { handler } from '@owlmeans/client'
|
|
4
|
+
import { bind, bindScreen, stab } from '@owlmeans/client-entrypoint'
|
|
5
|
+
import { AuthenticationHOC } from './components/authentication/component.js'
|
|
6
|
+
|
|
7
|
+
/** Browser entrypoints for the manager's authentication flows. */
|
|
8
|
+
export const entrypoints = [
|
|
9
|
+
bind(authProtocols.authen),
|
|
10
|
+
bind(authProtocols.init),
|
|
11
|
+
bind(authProtocols.authenticate),
|
|
12
|
+
bind(authProtocols.rely),
|
|
13
|
+
bind(authProtocols.client),
|
|
14
|
+
bind(authProtocols.login),
|
|
15
|
+
bindScreen(authProtocols.loginDefault, handler(AuthenticationHOC())),
|
|
16
|
+
bindScreen(authProtocols.loginTyped, handler(AuthenticationHOC())),
|
|
17
|
+
bindScreen(authProtocols.dispatcher, stab),
|
|
18
|
+
]
|
|
@@ -7,6 +7,13 @@ import { AuthenCredError } from '../errors.js'
|
|
|
7
7
|
export const ed25519BasicUIPlugin: AuthenticationPlugin = {
|
|
8
8
|
type: AuthenticationType.BasicEd25519,
|
|
9
9
|
|
|
10
|
+
method: { order: 300, icon: 'key', emphasis: 'secondary' },
|
|
11
|
+
|
|
12
|
+
// This package ships no UI family, so the form below is a `Renderer` a panel package assigns
|
|
13
|
+
// (`@owlmeans/web-panel/auth/plugins`, `@owlmeans/mui-panel/auth/plugins`). Until one does, the
|
|
14
|
+
// Implementation throws on mount — so the sign-in screen must not offer the method.
|
|
15
|
+
requiresRenderer: true,
|
|
16
|
+
|
|
10
17
|
Implementation: Renderer => ({ type, stage, control, params }) => {
|
|
11
18
|
|
|
12
19
|
type = type ?? AuthenticationType.BasicEd25519
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import type { ClientAuthType } from '../components/index.js'
|
|
2
|
-
|
|
3
1
|
import { AuthenticationType } from '@owlmeans/auth'
|
|
4
2
|
import { ed25519BasicUIPlugin } from './basic-ed25519.js'
|
|
5
|
-
import { AuthenticationPlugin } from './types.js'
|
|
6
3
|
import { reCaptchaPlugin } from './re-captcha.js'
|
|
7
4
|
import { tunnelConsumerUIPlugin } from './tunnel-consumer.js'
|
|
5
|
+
import { pluginMethodSource } from './methods.js'
|
|
6
|
+
import { registerMethodSource } from '../../login/methods.js'
|
|
7
|
+
import { plugins } from './registry.js'
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export { plugins, registerAuthPlugin, getAuthPlugin, listAuthPlugins } from './registry.js'
|
|
10
10
|
|
|
11
11
|
plugins[AuthenticationType.BasicEd25519] = ed25519BasicUIPlugin
|
|
12
12
|
plugins[AuthenticationType.ReCaptcha] = reCaptchaPlugin
|
|
13
13
|
plugins[AuthenticationType.WalletConsumer] = tunnelConsumerUIPlugin
|
|
14
|
+
|
|
15
|
+
// Registered from here, not from `login/`, so that importing `@owlmeans/client-auth/login` never
|
|
16
|
+
// drags this module's React plugin implementations into a bundle that only wanted the login host.
|
|
17
|
+
// A generated application imports the login subpath through `client-iam`; it must not gain three
|
|
18
|
+
// authentication methods it never registered as a side effect of that.
|
|
19
|
+
registerMethodSource(pluginMethodSource)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CAUTHEN_AUTHEN_TYPED } from '@owlmeans/auth'
|
|
2
|
+
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
3
|
+
import { DEFAULT_METHOD_ORDER, LOGIN_SERVICE } from '../../login/consts.js'
|
|
4
|
+
import type { LoginMethod, LoginMethodContext, LoginMethodSource, LoginService } from '../../login/types.js'
|
|
5
|
+
import type { AuthenticationPlugin } from './types.js'
|
|
6
|
+
import { listAuthPlugins } from './registry.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether a registered plugin may be put on the sign-in screen of THIS application.
|
|
10
|
+
*
|
|
11
|
+
* The registry is global and is filled by side-effect imports, so "registered" means only that
|
|
12
|
+
* some module was pulled into the bundle — three separate reasons why that is not enough:
|
|
13
|
+
*
|
|
14
|
+
* 1. No `method`: `re-captcha` is a STEP inside another flow, not a way to sign in.
|
|
15
|
+
* 2. No renderer: a plugin that declares `requiresRenderer` throws the moment its screen mounts
|
|
16
|
+
* unless a UI package (`web-panel`, `mui-panel`) assigned one. An app that imported the plugin
|
|
17
|
+
* host without that package would be offering buttons whose only outcome is a broken page.
|
|
18
|
+
* 3. Not wired: a plugin whose flow needs a service its context never appended fails on the
|
|
19
|
+
* lookup instead. Packages register their plugins in groups, so an app that wanted one of them
|
|
20
|
+
* gets the rest too, and the rest have to be able to say they do not apply here.
|
|
21
|
+
*/
|
|
22
|
+
const offerable = (plugin: AuthenticationPlugin, ctx: LoginMethodContext): boolean => {
|
|
23
|
+
if (plugin.method == null || plugin.method.hidden === true) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
if (plugin.requiresRenderer === true && plugin.Renderer == null) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return plugin.method.available?.(ctx) !== false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every registered authentication plugin that declares itself offerable, as a sign-in method.
|
|
35
|
+
*/
|
|
36
|
+
export const pluginMethodSource: LoginMethodSource = {
|
|
37
|
+
alias: 'authentication-plugins',
|
|
38
|
+
|
|
39
|
+
list: ctx => listAuthPlugins()
|
|
40
|
+
.filter(plugin => offerable(plugin, ctx))
|
|
41
|
+
.map((plugin): LoginMethod => {
|
|
42
|
+
const meta = plugin.method!
|
|
43
|
+
const type = plugin.type
|
|
44
|
+
const id = meta.id ?? type
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id, type,
|
|
48
|
+
...(meta.label != null ? { label: meta.label } : {}),
|
|
49
|
+
i18nKey: meta.i18nKey ?? id,
|
|
50
|
+
...(meta.icon != null ? { icon: meta.icon } : {}),
|
|
51
|
+
order: meta.order ?? DEFAULT_METHOD_ORDER,
|
|
52
|
+
...(meta.emphasis != null ? { emphasis: meta.emphasis } : {}),
|
|
53
|
+
...(meta.restricted === true ? { restricted: true } : {}),
|
|
54
|
+
params: { type },
|
|
55
|
+
|
|
56
|
+
// Non-async through the whole chain: `login.begin` opens the surrogate window in its first
|
|
57
|
+
// statement, and a window opened after the gesture has been handled is blocked.
|
|
58
|
+
start: methodCtx => {
|
|
59
|
+
const login = methodCtx.context.service<LoginService>(LOGIN_SERVICE)
|
|
60
|
+
const entrypoint = methodCtx.context.entrypoint<ClientEntrypoint>(CAUTHEN_AUTHEN_TYPED)
|
|
61
|
+
const url = entrypoint.path().replace(':type', encodeURIComponent(type))
|
|
62
|
+
|
|
63
|
+
return login.begin({
|
|
64
|
+
url,
|
|
65
|
+
// In an ordinary tab this keeps the application mounted, exactly as the pre-chooser
|
|
66
|
+
// dispatcher did; the surrogate plugin ignores it and opens a window instead.
|
|
67
|
+
...(methodCtx.navigate != null
|
|
68
|
+
? { navigate: () => methodCtx.navigate?.(CAUTHEN_AUTHEN_TYPED, { type }) }
|
|
69
|
+
: {}),
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
@@ -5,6 +5,13 @@ import { useEffect } from 'react'
|
|
|
5
5
|
export const reCaptchaPlugin: AuthenticationPlugin = {
|
|
6
6
|
type: AuthenticationType.ReCaptcha,
|
|
7
7
|
|
|
8
|
+
// A STEP inside another flow, never a way to sign in — so it is registered and never offered.
|
|
9
|
+
method: { hidden: true },
|
|
10
|
+
|
|
11
|
+
// Its widget is a `Renderer` a panel package assigns. `hidden` already keeps it off the sign-in
|
|
12
|
+
// screen; this is what the flow that DOES drive it can check before handing it a stage.
|
|
13
|
+
requiresRenderer: true,
|
|
14
|
+
|
|
8
15
|
Implementation: Renderer => ({ type, stage, control, params }) => {
|
|
9
16
|
Renderer = Renderer ?? reCaptchaPlugin.Renderer
|
|
10
17
|
|