@owlmeans/mui-oidc-rp 0.1.3
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/LICENSE +21 -0
- package/README.md +77 -0
- package/build/auth/plugins/google-client.d.ts +3 -0
- package/build/auth/plugins/google-client.d.ts.map +1 -0
- package/build/auth/plugins/google-client.js +71 -0
- package/build/auth/plugins/google-client.js.map +1 -0
- package/build/auth/plugins/helpers.d.ts +16 -0
- package/build/auth/plugins/helpers.d.ts.map +1 -0
- package/build/auth/plugins/helpers.js +31 -0
- package/build/auth/plugins/helpers.js.map +1 -0
- package/build/auth/plugins/index.d.ts +4 -0
- package/build/auth/plugins/index.d.ts.map +1 -0
- package/build/auth/plugins/index.js +8 -0
- package/build/auth/plugins/index.js.map +1 -0
- package/build/auth/plugins/oidc-client.d.ts +3 -0
- package/build/auth/plugins/oidc-client.d.ts.map +1 -0
- package/build/auth/plugins/oidc-client.js +101 -0
- package/build/auth/plugins/oidc-client.js.map +1 -0
- package/build/components/dispatcher.d.ts +2 -0
- package/build/components/dispatcher.d.ts.map +1 -0
- package/build/components/dispatcher.js +58 -0
- package/build/components/dispatcher.js.map +1 -0
- package/build/components/index.d.ts +2 -0
- package/build/components/index.d.ts.map +1 -0
- package/build/components/index.js +2 -0
- package/build/components/index.js.map +1 -0
- package/build/consts.d.ts +7 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +8 -0
- package/build/consts.js.map +1 -0
- package/build/guard.d.ts +7 -0
- package/build/guard.d.ts.map +1 -0
- package/build/guard.js +19 -0
- package/build/guard.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/service.d.ts +3 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +123 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +64 -0
- package/src/auth/plugins/google-client.tsx +94 -0
- package/src/auth/plugins/helpers.ts +39 -0
- package/src/auth/plugins/index.ts +10 -0
- package/src/auth/plugins/oidc-client.tsx +118 -0
- package/src/components/dispatcher.tsx +61 -0
- package/src/components/index.ts +2 -0
- package/src/consts.ts +8 -0
- package/src/guard.ts +34 -0
- package/src/index.ts +6 -0
- package/src/service.ts +159 -0
- package/src/types.ts +39 -0
- package/tests/context.ts +16 -0
- package/tests/google-client.spec.ts +64 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +11 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Config, Context, OidcAuthService, OidcInteraction } from './types.js'
|
|
2
|
+
import type { FlowService } from '@owlmeans/client-flow'
|
|
3
|
+
|
|
4
|
+
import { assertContext, createService, HOME } from '@owlmeans/context'
|
|
5
|
+
import { DEFAULT_ALIAS } from './consts.js'
|
|
6
|
+
import { UserManager } from 'oidc-client-ts'
|
|
7
|
+
import { DEFAULT_ALIAS as FLOW_SERVICE } from '@owlmeans/client-flow'
|
|
8
|
+
import { FlowStepMissconfigured, OidcAuthStep, STD_OIDC_FLOW, UnknownFlow } from '@owlmeans/flow'
|
|
9
|
+
import type { Module } from '@owlmeans/web-client'
|
|
10
|
+
import { DISPATCHER_OIDC, DISPATCHER_OIDC_INIT, OIDC_CODE_QUERY } from '@owlmeans/oidc'
|
|
11
|
+
import type { Auth, AuthToken } from '@owlmeans/auth'
|
|
12
|
+
import { AUTH_RESOURCE, USER_ID } from '@owlmeans/client-auth'
|
|
13
|
+
import type { ClientAuthResource } from '@owlmeans/client-auth'
|
|
14
|
+
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
|
|
15
|
+
|
|
16
|
+
export const makeOidcAuthService = (alias: string = DEFAULT_ALIAS): OidcAuthService => {
|
|
17
|
+
const store = (context: Context) => context.auth().store<OidcInteraction>()
|
|
18
|
+
const storeKey = '_oidc-client-interaction'
|
|
19
|
+
|
|
20
|
+
const service: OidcAuthService = createService<OidcAuthService>(alias, {
|
|
21
|
+
/**
|
|
22
|
+
* This method performs when the IdP returns authentication params.
|
|
23
|
+
*/
|
|
24
|
+
dispatch: async params => {
|
|
25
|
+
/**
|
|
26
|
+
* 1. Check if there are parameters in the URL for the server side OIDC authentication (e.g. code)
|
|
27
|
+
* 2. Send paramters to finalize authentication (proxy request to the auth server?)
|
|
28
|
+
* 3. If the authentication successful set user state using authentication service
|
|
29
|
+
*/
|
|
30
|
+
if (params[OIDC_CODE_QUERY] == null) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ctx = service.assertCtx<Config, Context>()
|
|
35
|
+
|
|
36
|
+
params.authUrl = (await store(ctx).get(storeKey)).authUrl
|
|
37
|
+
|
|
38
|
+
const [authToken] = await ctx.module<Module<AuthToken>>(DISPATCHER_OIDC)
|
|
39
|
+
.call({ body: params })
|
|
40
|
+
|
|
41
|
+
if (authToken.token != null && authToken.token !== '') {
|
|
42
|
+
const authResource = ctx.resource<ClientAuthResource>(AUTH_RESOURCE)
|
|
43
|
+
await authResource.save({ id: USER_ID, token: authToken.token })
|
|
44
|
+
|
|
45
|
+
const [, authorization] = authToken.token.split(' ')
|
|
46
|
+
const envelope = makeEnvelopeModel<Auth>(authorization, EnvelopeKind.Token)
|
|
47
|
+
|
|
48
|
+
ctx.auth().auth = envelope.message()
|
|
49
|
+
ctx.auth().token = authToken.token
|
|
50
|
+
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
authenticate: async (flow, params) => {
|
|
58
|
+
/**
|
|
59
|
+
* 1. Kick of OIDC authentication if we receive some hints about who or for what organization we'd like to authenticate
|
|
60
|
+
* 2. Request OIDC authentication params from the server using these entity and profile id
|
|
61
|
+
* 3. The server should return an URL to redirect user to
|
|
62
|
+
* 4. Return this user to the component for the further processing (redirection)
|
|
63
|
+
*/
|
|
64
|
+
const state = flow.state()
|
|
65
|
+
if (state.flow !== STD_OIDC_FLOW || state.step !== OidcAuthStep.Ephemeral) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
if (params.entity == null && state.entityId == null) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
params.entity ??= state.entityId!
|
|
72
|
+
|
|
73
|
+
const ctx = service.assertCtx<Config, Context>()
|
|
74
|
+
|
|
75
|
+
let [redirectTo] = await ctx.module<Module<string>>(DISPATCHER_OIDC_INIT)
|
|
76
|
+
.call({ body: params })
|
|
77
|
+
|
|
78
|
+
if (flow.payload().simplified === 'true') {
|
|
79
|
+
redirectTo += '&simplified=true'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const context = service.assertCtx<Config, Context>()
|
|
83
|
+
await store(context).save({ id: storeKey, authUrl: redirectTo })
|
|
84
|
+
|
|
85
|
+
return redirectTo
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
proceedToRedirectUrl: async extras => {
|
|
89
|
+
const context = assertContext<Config, Context>(service.ctx as Context)
|
|
90
|
+
|
|
91
|
+
const flow = context.service<FlowService>(FLOW_SERVICE)
|
|
92
|
+
const flowModel = await flow.state()
|
|
93
|
+
if (flowModel == null) {
|
|
94
|
+
throw new UnknownFlow('oidc.dispatch')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const authorityTransition = flowModel.next()
|
|
98
|
+
flowModel.transit(authorityTransition.transition, true, {
|
|
99
|
+
purpose: extras.purpose,
|
|
100
|
+
simplified: extras.simplified,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// @TODO I'm not sure that this dirty hack is a correct approach
|
|
104
|
+
if (extras.alias === HOME) {
|
|
105
|
+
const redirectTransition = flowModel.next()
|
|
106
|
+
flowModel.transit(redirectTransition.transition, true, {
|
|
107
|
+
purpose: extras.purpose,
|
|
108
|
+
simplified: extras.simplified
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const redirectUrl = await flow.proceed({ params: { uid: extras.uid } }, true)
|
|
113
|
+
|
|
114
|
+
return redirectUrl
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* This is client-side only OIDC implementation. It's under constructuion.
|
|
119
|
+
* We stoped on the try to redirect to OIDC provider, but it requires browser integrated
|
|
120
|
+
* cryptography under ssl/tls.
|
|
121
|
+
* @TODO Finish client-side only implementation one day
|
|
122
|
+
*/
|
|
123
|
+
dispatchClientOnly: async () => {
|
|
124
|
+
const context = assertContext<Config, Context>(service.ctx as Context)
|
|
125
|
+
|
|
126
|
+
const flow = context.service<FlowService>(FLOW_SERVICE)
|
|
127
|
+
|
|
128
|
+
const flowModel = await flow.state()
|
|
129
|
+
if (flowModel == null) {
|
|
130
|
+
throw new UnknownFlow('oidc.dispatch')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const authorityTransition = flowModel.next()
|
|
134
|
+
flowModel.transit(authorityTransition.transition, true)
|
|
135
|
+
const authorityStep = flowModel.step()
|
|
136
|
+
if (authorityStep.module == null) {
|
|
137
|
+
throw new FlowStepMissconfigured(authorityStep.step)
|
|
138
|
+
}
|
|
139
|
+
const [authorityUrl] = await context.module<Module>(authorityStep.module).call<string>()
|
|
140
|
+
|
|
141
|
+
const redirectTransition = flowModel.next()
|
|
142
|
+
flowModel.transit(redirectTransition.transition, true)
|
|
143
|
+
|
|
144
|
+
const redirectUrl = await flow.proceed(undefined, true)
|
|
145
|
+
|
|
146
|
+
// @TODO To proceed we need to provide client_id some way
|
|
147
|
+
const manager = new UserManager({
|
|
148
|
+
// client_id: context.cfg.oidc.consumer?.clientId ?? '',
|
|
149
|
+
client_id: '',
|
|
150
|
+
redirect_uri: redirectUrl,
|
|
151
|
+
authority: authorityUrl,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
await manager.signinRedirect({ state: flowModel.serialize() })
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return service
|
|
159
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InitializedService } from '@owlmeans/context'
|
|
2
|
+
import type { AppConfig, AppContext } from '@owlmeans/web-client'
|
|
3
|
+
import type { OIDCAuthInitParams, WithSharedConfig } from '@owlmeans/oidc'
|
|
4
|
+
import type { OidcAuthPurposes } from './consts.js'
|
|
5
|
+
import type { FlowModel, FlowPayload } from '@owlmeans/flow'
|
|
6
|
+
import type { ResourceRecord } from '@owlmeans/resource'
|
|
7
|
+
import type { AuthToken } from '@owlmeans/auth'
|
|
8
|
+
|
|
9
|
+
export interface OidcAuthService extends InitializedService {
|
|
10
|
+
dispatch: (params: Record<string, string>) => Promise<boolean>
|
|
11
|
+
|
|
12
|
+
authenticate: (flow: FlowModel, params: OIDCAuthInitParams) => Promise<string | null>
|
|
13
|
+
|
|
14
|
+
// This is dedicated authentication service implementation
|
|
15
|
+
proceedToRedirectUrl: (extras: OidcAuthRedirectExtras) => Promise<string>
|
|
16
|
+
|
|
17
|
+
// @TODO Unfinished implementation
|
|
18
|
+
dispatchClientOnly: () => Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OidcAuthRedirectExtras extends FlowPayload {
|
|
22
|
+
purpose: OidcAuthPurposes
|
|
23
|
+
simplified?: string
|
|
24
|
+
uid?: string
|
|
25
|
+
alias?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OidcPostAuthPayload extends FlowPayload , AuthToken {
|
|
29
|
+
purpose: OidcAuthPurposes
|
|
30
|
+
simplified?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Config extends AppConfig, WithSharedConfig { }
|
|
34
|
+
|
|
35
|
+
export interface Context<C extends Config = Config> extends AppContext<C> { }
|
|
36
|
+
|
|
37
|
+
export interface OidcInteraction extends ResourceRecord {
|
|
38
|
+
authUrl: string
|
|
39
|
+
}
|
package/tests/context.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AppType, Layer, makeBasicContext } from '@owlmeans/context'
|
|
2
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
|
+
|
|
4
|
+
export const makeTestContext = () => {
|
|
5
|
+
const cfg: BasicConfig = {
|
|
6
|
+
ready: false,
|
|
7
|
+
service: 'web-oidc-rp-tests',
|
|
8
|
+
layer: Layer.Application,
|
|
9
|
+
type: AppType.Frontend,
|
|
10
|
+
services: {},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const context = makeBasicContext(cfg) as BasicContext<BasicConfig>
|
|
14
|
+
|
|
15
|
+
return context
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { extractGoogleUrl, buildCallbackCredentials } from '../src/auth/plugins/helpers.js'
|
|
3
|
+
import { AUTH_SCOPE, AuthRole } from '@owlmeans/auth'
|
|
4
|
+
import { makeFixtureKeyPair, signMockEnvelope } from '@owlmeans/test-auth'
|
|
5
|
+
import { EnvelopeKind } from '@owlmeans/basic-envelope'
|
|
6
|
+
|
|
7
|
+
describe('@owlmeans/mui-oidc-rp — extractGoogleUrl', () => {
|
|
8
|
+
test('strips source prefix from envelope message', async () => {
|
|
9
|
+
const kp = makeFixtureKeyPair('web-oidc-test')
|
|
10
|
+
const googleUrl = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&state=xyz'
|
|
11
|
+
const source = 'https://example.com/auth'
|
|
12
|
+
const challengeMsg = `${source}:${googleUrl}`
|
|
13
|
+
// Create a signed envelope wrapping the prefixed URL
|
|
14
|
+
const envelope = await signMockEnvelope(challengeMsg, 'string', EnvelopeKind.Wrap, kp)
|
|
15
|
+
|
|
16
|
+
const result = extractGoogleUrl(envelope, source)
|
|
17
|
+
|
|
18
|
+
expect(result).toBe(googleUrl)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('handles raw URL without source prefix', async () => {
|
|
22
|
+
const kp = makeFixtureKeyPair('web-oidc-test-2')
|
|
23
|
+
const googleUrl = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&state=xyz'
|
|
24
|
+
const envelope = await signMockEnvelope(googleUrl, 'string', EnvelopeKind.Wrap, kp)
|
|
25
|
+
|
|
26
|
+
const result = extractGoogleUrl(envelope, 'https://different.example.com')
|
|
27
|
+
|
|
28
|
+
expect(result).toBe(googleUrl)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('handles empty source prefix', async () => {
|
|
32
|
+
const kp = makeFixtureKeyPair('web-oidc-test-3')
|
|
33
|
+
const googleUrl = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=abc'
|
|
34
|
+
const envelope = await signMockEnvelope(googleUrl, 'string', EnvelopeKind.Wrap, kp)
|
|
35
|
+
|
|
36
|
+
const result = extractGoogleUrl(envelope, '')
|
|
37
|
+
|
|
38
|
+
expect(result).toBe(googleUrl)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('@owlmeans/mui-oidc-rp — buildCallbackCredentials', () => {
|
|
43
|
+
test('builds AuthCredentials from query params', () => {
|
|
44
|
+
const queryString = 'code=4/0abc123&state=test-state-xyz&scope=openid+email'
|
|
45
|
+
const type = 'google-oauth'
|
|
46
|
+
const challenge = 'stored-challenge-value'
|
|
47
|
+
|
|
48
|
+
const creds = buildCallbackCredentials(queryString, type, challenge)
|
|
49
|
+
|
|
50
|
+
expect(creds.type).toBe('google-oauth')
|
|
51
|
+
expect(creds.challenge).toBe('stored-challenge-value')
|
|
52
|
+
expect(creds.credential).toBe(queryString)
|
|
53
|
+
expect(creds.role).toBe(AuthRole.User)
|
|
54
|
+
expect(creds.userId).toBe('code')
|
|
55
|
+
expect(creds.scopes).toEqual([AUTH_SCOPE])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('works with empty challenge', () => {
|
|
59
|
+
const creds = buildCallbackCredentials('code=x&state=y', 'google-oauth', '')
|
|
60
|
+
|
|
61
|
+
expect(creds.challenge).toBe('')
|
|
62
|
+
expect(creds.credential).toBe('code=x&state=y')
|
|
63
|
+
})
|
|
64
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.react.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
|
+
},
|
|
10
|
+
"exclude": ["./dist/**/*", "./build/**/*", "./tests/**/*", "./*.ts"]
|
|
11
|
+
}
|