@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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/build/auth/plugins/google-client.d.ts +3 -0
  4. package/build/auth/plugins/google-client.d.ts.map +1 -0
  5. package/build/auth/plugins/google-client.js +71 -0
  6. package/build/auth/plugins/google-client.js.map +1 -0
  7. package/build/auth/plugins/helpers.d.ts +16 -0
  8. package/build/auth/plugins/helpers.d.ts.map +1 -0
  9. package/build/auth/plugins/helpers.js +31 -0
  10. package/build/auth/plugins/helpers.js.map +1 -0
  11. package/build/auth/plugins/index.d.ts +4 -0
  12. package/build/auth/plugins/index.d.ts.map +1 -0
  13. package/build/auth/plugins/index.js +8 -0
  14. package/build/auth/plugins/index.js.map +1 -0
  15. package/build/auth/plugins/oidc-client.d.ts +3 -0
  16. package/build/auth/plugins/oidc-client.d.ts.map +1 -0
  17. package/build/auth/plugins/oidc-client.js +101 -0
  18. package/build/auth/plugins/oidc-client.js.map +1 -0
  19. package/build/components/dispatcher.d.ts +2 -0
  20. package/build/components/dispatcher.d.ts.map +1 -0
  21. package/build/components/dispatcher.js +58 -0
  22. package/build/components/dispatcher.js.map +1 -0
  23. package/build/components/index.d.ts +2 -0
  24. package/build/components/index.d.ts.map +1 -0
  25. package/build/components/index.js +2 -0
  26. package/build/components/index.js.map +1 -0
  27. package/build/consts.d.ts +7 -0
  28. package/build/consts.d.ts.map +1 -0
  29. package/build/consts.js +8 -0
  30. package/build/consts.js.map +1 -0
  31. package/build/guard.d.ts +7 -0
  32. package/build/guard.d.ts.map +1 -0
  33. package/build/guard.js +19 -0
  34. package/build/guard.js.map +1 -0
  35. package/build/index.d.ts +6 -0
  36. package/build/index.d.ts.map +1 -0
  37. package/build/index.js +5 -0
  38. package/build/index.js.map +1 -0
  39. package/build/service.d.ts +3 -0
  40. package/build/service.d.ts.map +1 -0
  41. package/build/service.js +123 -0
  42. package/build/service.js.map +1 -0
  43. package/build/types.d.ts +31 -0
  44. package/build/types.d.ts.map +1 -0
  45. package/build/types.js +2 -0
  46. package/build/types.js.map +1 -0
  47. package/package.json +64 -0
  48. package/src/auth/plugins/google-client.tsx +94 -0
  49. package/src/auth/plugins/helpers.ts +39 -0
  50. package/src/auth/plugins/index.ts +10 -0
  51. package/src/auth/plugins/oidc-client.tsx +118 -0
  52. package/src/components/dispatcher.tsx +61 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/consts.ts +8 -0
  55. package/src/guard.ts +34 -0
  56. package/src/index.ts +6 -0
  57. package/src/service.ts +159 -0
  58. package/src/types.ts +39 -0
  59. package/tests/context.ts +16 -0
  60. package/tests/google-client.spec.ts +64 -0
  61. package/tests/tsconfig.json +12 -0
  62. 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
+ }
@@ -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
+ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.react.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "types": ["bun"],
8
+ "rootDir": "../",
9
+ "noEmit": true
10
+ },
11
+ "include": ["./**/*", "../src/**/*"]
12
+ }
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
+ }