@owlmeans/server-oidc-rp 0.1.2 → 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 (63) hide show
  1. package/README.md +59 -474
  2. package/build/actions/init.d.ts +2 -2
  3. package/build/actions/init.d.ts.map +1 -1
  4. package/build/actions/init.js +2 -2
  5. package/build/actions/init.js.map +1 -1
  6. package/build/actions/process.d.ts +2 -2
  7. package/build/actions/process.d.ts.map +1 -1
  8. package/build/actions/process.js.map +1 -1
  9. package/build/auth/plugins/google-client.d.ts +4 -0
  10. package/build/auth/plugins/google-client.d.ts.map +1 -0
  11. package/build/auth/plugins/google-client.js +147 -0
  12. package/build/auth/plugins/google-client.js.map +1 -0
  13. package/build/auth/plugins/index.d.ts.map +1 -1
  14. package/build/auth/plugins/index.js +3 -1
  15. package/build/auth/plugins/index.js.map +1 -1
  16. package/build/gate.d.ts +1 -1
  17. package/build/gate.d.ts.map +1 -1
  18. package/build/guard.d.ts +2 -2
  19. package/build/guard.d.ts.map +1 -1
  20. package/build/guard.js +1 -1
  21. package/build/guard.js.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js.map +1 -1
  25. package/build/model/gate.js +1 -1
  26. package/build/model/gate.js.map +1 -1
  27. package/build/modules.d.ts +2 -2
  28. package/build/modules.d.ts.map +1 -1
  29. package/build/modules.js +3 -3
  30. package/build/modules.js.map +1 -1
  31. package/build/service.d.ts.map +1 -1
  32. package/build/service.js +15 -7
  33. package/build/service.js.map +1 -1
  34. package/build/types.d.ts +44 -7
  35. package/build/types.d.ts.map +1 -1
  36. package/build/utils/auth.d.ts +2 -2
  37. package/build/utils/auth.d.ts.map +1 -1
  38. package/build/utils/auth.js.map +1 -1
  39. package/build/utils/types.d.ts +3 -2
  40. package/build/utils/types.d.ts.map +1 -1
  41. package/build/wrapper.js +2 -2
  42. package/build/wrapper.js.map +1 -1
  43. package/package.json +24 -20
  44. package/src/actions/init.ts +5 -5
  45. package/src/actions/process.ts +2 -2
  46. package/src/auth/plugins/google-client.ts +189 -0
  47. package/src/auth/plugins/index.ts +3 -1
  48. package/src/gate.ts +1 -1
  49. package/src/guard.ts +3 -3
  50. package/src/index.ts +17 -1
  51. package/src/model/gate.ts +2 -2
  52. package/src/modules.ts +6 -6
  53. package/src/service.ts +22 -12
  54. package/src/types.ts +67 -8
  55. package/src/utils/auth.ts +2 -3
  56. package/src/utils/types.ts +3 -2
  57. package/src/wrapper.ts +8 -8
  58. package/tests/context.ts +44 -0
  59. package/tests/google-client.spec.ts +123 -0
  60. package/tests/service.spec.ts +75 -0
  61. package/tests/tsconfig.json +12 -0
  62. package/tsconfig.json +6 -5
  63. package/build/.gitkeep +0 -0
package/src/utils/auth.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import type { AuthCredentials } from '@owlmeans/auth'
2
2
  import { AuthenFailed, AuthenPayloadError } from '@owlmeans/auth'
3
- import type { Config, Context, OidcClientService, TokenSet } from '../types.js'
3
+ import type { Config, Context, OidcClientService, OidcTokenSet } from '../types.js'
4
4
  import type { OidcProviderConfig } from '@owlmeans/oidc'
5
- import type { } from 'openid-client'
6
5
  import Url from 'url'
7
6
  import { cache, exchangeId, verifierId } from './cache.js'
8
7
  import { DEFAULT_ALIAS } from '../consts.js'
@@ -11,7 +10,7 @@ import { randomBytes } from '@noble/hashes/utils'
11
10
  import { AUTHEN_TIMEFRAME } from '@owlmeans/server-auth'
12
11
 
13
12
  export const makeOidcAuthentication = <C extends Config, T extends Context<C>>(context: T) =>
14
- async (credential: AuthCredentials): Promise<[OidcProviderConfig, TokenSet, string]> => {
13
+ async (credential: AuthCredentials): Promise<[OidcProviderConfig, OidcTokenSet, string]> => {
15
14
  console.log('Challenge we got: ', credential.challenge)
16
15
  const challengeParts = credential.challenge.split(':http')
17
16
  // This is ex. source - url we were going to return user to after all it happens
@@ -1,10 +1,11 @@
1
1
  import type { AuthSpent } from '@owlmeans/server-auth'
2
- import type { TokenSetParameters } from '../types.js'
2
+ import type { OidcTokenSetParameters } from '../types.js'
3
3
 
4
4
  export interface OIDCAuthCache extends AuthSpent {
5
5
  verifier?: string
6
- payload?: TokenSetParameters
6
+ payload?: OidcTokenSetParameters
7
7
  client?: string
8
8
  validated?: Date
9
9
  entityId?: string
10
+ redirectUri?: string
10
11
  }
package/src/wrapper.ts CHANGED
@@ -5,10 +5,10 @@ import { createService } from '@owlmeans/context'
5
5
  import type { CommonTokenSetParams, OIDCTokenUpdate, WrappedOIDCService } from '@owlmeans/oidc'
6
6
  import { OIDC_WRAPPED_TOKEN, WRAPPED_OIDC } from '@owlmeans/oidc'
7
7
  import { cache, managedId } from './utils/cache.js'
8
- import type { Config, Context, OidcClientService, TokenSetParameters } from './types.js'
8
+ import type { Config, Context, OidcClientService, OidcTokenSetParameters } from './types.js'
9
9
  import { authService, DEFAULT_ALIAS, OIDC_AUTH_LIFTETIME, OIDC_WRAP_FRESHNESS } from './consts.js'
10
10
  import days from 'dayjs'
11
- import type { ClientModule } from '@owlmeans/client-module'
11
+ import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
12
12
  import { TRUSTED } from '@owlmeans/config'
13
13
  import { AUTH_SRV_KEY } from '@owlmeans/server-auth'
14
14
  import { trust } from '@owlmeans/auth-common/utils'
@@ -38,8 +38,8 @@ export const makeOidcWrappingService = (): WrappedOIDCService => {
38
38
 
39
39
  record.validated = new Date()
40
40
 
41
- if (ctx.hasModule(authService.auth.update)) {
42
- const [update] = await ctx.module<ClientModule<OIDCTokenUpdate>>(authService.auth.update)
41
+ if (ctx.hasEntrypoint(authService.auth.update)) {
42
+ const [update] = await ctx.entrypoint<ClientEntrypoint<OIDCTokenUpdate>>(authService.auth.update)
43
43
  .call({ body: { token, tokenSet: record.payload } })
44
44
 
45
45
  const updateEnvelope = makeEnvelopeModel<AuthCredentials>(update.token, EnvelopeKind.Token)
@@ -70,7 +70,7 @@ export const makeOidcWrappingService = (): WrappedOIDCService => {
70
70
  const authorization = await makeEnvelopeModel<Auth>(OIDC_WRAPPED_TOKEN)
71
71
  .send(updatedUser, null).sign(trusted.key, EnvelopeKind.Token)
72
72
 
73
- record.payload = update.tokenSet as TokenSetParameters
73
+ record.payload = update.tokenSet as OidcTokenSetParameters
74
74
 
75
75
  await cache(ctx).save(record, { ttl: OIDC_AUTH_LIFTETIME / 1000 })
76
76
 
@@ -82,9 +82,9 @@ export const makeOidcWrappingService = (): WrappedOIDCService => {
82
82
  let tokenSet: CommonTokenSetParams = record.payload as CommonTokenSetParams
83
83
  const client = await oidc.getClient(defaultClientId)
84
84
  if (days.unix(tokenSet.expires_at ?? 0).isBefore()) {
85
- tokenSet = await client.refresh(tokenSet as TokenSetParameters) as CommonTokenSetParams
85
+ tokenSet = await client.refresh(tokenSet as OidcTokenSetParameters) as CommonTokenSetParams
86
86
  } else {
87
- const result = await client.introspect(tokenSet as TokenSetParameters, 'access_token')
87
+ const result = await client.introspect(tokenSet as OidcTokenSetParameters, 'access_token')
88
88
  if (!result.active) {
89
89
  throw new AuthorizationError('access-token')
90
90
  }
@@ -99,7 +99,7 @@ export const makeOidcWrappingService = (): WrappedOIDCService => {
99
99
  const authorization = await makeEnvelopeModel<Auth>(OIDC_WRAPPED_TOKEN)
100
100
  .send(updatedUser, null).sign(trusted.key, EnvelopeKind.Token)
101
101
 
102
- record.payload = tokenSet as TokenSetParameters
102
+ record.payload = tokenSet as OidcTokenSetParameters
103
103
 
104
104
  await cache(ctx).save(record, { ttl: OIDC_AUTH_LIFTETIME / 1000 })
105
105
 
@@ -0,0 +1,44 @@
1
+ import { AppType, Layer, makeBasicContext } from '@owlmeans/context'
2
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
3
+ import { createStaticResource } from '@owlmeans/static-resource'
4
+ import { AUTH_CACHE } from '@owlmeans/server-auth'
5
+ import { makeOidcClientService } from '../src/service.js'
6
+ import { GOOGLE_SERVICE } from '@owlmeans/oidc'
7
+ import type { Config } from '../src/types.js'
8
+
9
+ const RESOURCE_KEY = 'server-oidc-rp-test-cache'
10
+
11
+ export const makeTestContext = () => {
12
+ const cfg: Config = {
13
+ ready: false,
14
+ service: 'server-oidc-rp-tests',
15
+ layer: Layer.Service,
16
+ type: AppType.Backend,
17
+ services: {},
18
+ oidc: {
19
+ providers: [
20
+ {
21
+ clientId: 'google-client-id-123',
22
+ secret: 'google-secret-456',
23
+ service: GOOGLE_SERVICE,
24
+ basePath: 'unused-in-unit-tests',
25
+ },
26
+ {
27
+ clientId: 'keycloak-admin',
28
+ secret: 'kc-secret',
29
+ service: 'iam-product-api',
30
+ basePath: 'realms/master',
31
+ def: true,
32
+ internal: true,
33
+ },
34
+ ],
35
+ },
36
+ } as Config
37
+
38
+ const context = makeBasicContext(cfg) as BasicContext<Config>
39
+
40
+ context.registerResource(createStaticResource(AUTH_CACHE, RESOURCE_KEY))
41
+ context.registerService(makeOidcClientService())
42
+
43
+ return context
44
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { makeTestContext } from './context.js'
3
+ import { googleClientPlugin } from '../src/auth/plugins/google-client.js'
4
+ import { GOOGLE_SERVICE } from '@owlmeans/oidc'
5
+ import { AuthenPayloadError, AuthRole } from '@owlmeans/auth'
6
+ import { UnknownRecordError } from '@owlmeans/resource'
7
+ import { verifierId } from '../src/utils/cache.js'
8
+ import { AUTH_CACHE } from '@owlmeans/server-auth'
9
+ import type { Resource } from '@owlmeans/resource'
10
+ import type { OIDCAuthCache } from '../src/utils/types.js'
11
+ import type { Config, Context } from '../src/types.js'
12
+ import type { BasicContext } from '@owlmeans/context'
13
+
14
+ const initContext = async () => {
15
+ const ctx = makeTestContext()
16
+ ctx.configure()
17
+ await ctx.init()
18
+ return ctx
19
+ }
20
+
21
+ describe('@owlmeans/server-oidc-rp — googleClientPlugin.init', () => {
22
+ test('rejects when request.source is missing', async () => {
23
+ const ctx = await initContext()
24
+ const plugin = googleClientPlugin(ctx as unknown as Context, GOOGLE_SERVICE)
25
+
26
+ await expect(
27
+ plugin.init({ type: 'google-oauth', source: undefined as any })
28
+ ).rejects.toBeInstanceOf(AuthenPayloadError)
29
+ })
30
+
31
+ test('returns a Google auth URL with correct params', async () => {
32
+ const ctx = await initContext()
33
+ const plugin = googleClientPlugin(ctx as unknown as Context, GOOGLE_SERVICE)
34
+
35
+ const result = await plugin.init({
36
+ type: 'google-oauth',
37
+ source: 'https://example.com/authentication/login/google-oauth',
38
+ })
39
+
40
+ expect(result.challenge).toContain('https://accounts.google.com/o/oauth2/v2/auth')
41
+
42
+ const url = new URL(result.challenge)
43
+ expect(url.searchParams.get('client_id')).toBe('google-client-id-123')
44
+ expect(url.searchParams.get('redirect_uri')).toBe('https://example.com/authentication/login/google-oauth')
45
+ expect(url.searchParams.get('response_type')).toBe('code')
46
+ expect(url.searchParams.get('code_challenge_method')).toBe('S256')
47
+ expect(url.searchParams.get('state')).not.toBeNull()
48
+ expect(url.searchParams.get('state')!.length).toBeGreaterThan(0)
49
+ expect(url.searchParams.get('code_challenge')).not.toBeNull()
50
+ })
51
+
52
+ test('caches PKCE verifier under verifierId(state) with redirectUri', async () => {
53
+ const ctx = await initContext()
54
+ const plugin = googleClientPlugin(ctx as unknown as Context, GOOGLE_SERVICE)
55
+
56
+ const result = await plugin.init({
57
+ type: 'google-oauth',
58
+ source: 'https://example.com/auth/login/google-oauth',
59
+ })
60
+
61
+ const url = new URL(result.challenge)
62
+ const state = url.searchParams.get('state')!
63
+
64
+ const cache = (ctx as unknown as BasicContext<Config>).resource<Resource<OIDCAuthCache>>(AUTH_CACHE)
65
+ const record = await cache.load(verifierId(state))
66
+
67
+ expect(record).not.toBeNull()
68
+ expect(record!.verifier).toBeDefined()
69
+ expect(record!.verifier!.length).toBeGreaterThan(0)
70
+ expect(record!.client).toBe('google-client-id-123')
71
+ expect(record!.redirectUri).toBe('https://example.com/auth/login/google-oauth')
72
+ })
73
+ })
74
+
75
+ describe('@owlmeans/server-oidc-rp — googleClientPlugin.authenticate', () => {
76
+ test('rejects when code is missing', async () => {
77
+ const ctx = await initContext()
78
+ const plugin = googleClientPlugin(ctx as unknown as Context, GOOGLE_SERVICE)
79
+
80
+ await expect(
81
+ plugin.authenticate({
82
+ type: 'google-oauth',
83
+ challenge: '',
84
+ credential: 'state=abc123',
85
+ role: AuthRole.User,
86
+ userId: 'code',
87
+ scopes: ['*'],
88
+ })
89
+ ).rejects.toBeInstanceOf(AuthenPayloadError)
90
+ })
91
+
92
+ test('rejects when state is missing', async () => {
93
+ const ctx = await initContext()
94
+ const plugin = googleClientPlugin(ctx as unknown as Context, GOOGLE_SERVICE)
95
+
96
+ await expect(
97
+ plugin.authenticate({
98
+ type: 'google-oauth',
99
+ challenge: '',
100
+ credential: 'code=abc123',
101
+ role: AuthRole.User,
102
+ userId: 'code',
103
+ scopes: ['*'],
104
+ })
105
+ ).rejects.toBeInstanceOf(AuthenPayloadError)
106
+ })
107
+
108
+ test('rejects when verifier not found for state', async () => {
109
+ const ctx = await initContext()
110
+ const plugin = googleClientPlugin(ctx as unknown as Context, GOOGLE_SERVICE)
111
+
112
+ await expect(
113
+ plugin.authenticate({
114
+ type: 'google-oauth',
115
+ challenge: '',
116
+ credential: 'code=abc123&state=nonexistent-state',
117
+ role: AuthRole.User,
118
+ userId: 'code',
119
+ scopes: ['*'],
120
+ })
121
+ ).rejects.toBeInstanceOf(UnknownRecordError)
122
+ })
123
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { makeTestContext } from './context.js'
3
+ import { GOOGLE_SERVICE } from '@owlmeans/oidc'
4
+ import { AuthManagerError } from '@owlmeans/auth'
5
+ import type { OidcClientService } from '../src/types.js'
6
+ import { DEFAULT_ALIAS } from '../src/consts.js'
7
+
8
+ describe('@owlmeans/server-oidc-rp — getConfig', () => {
9
+ test('finds provider by service name only', async () => {
10
+ const ctx = makeTestContext()
11
+ ctx.configure()
12
+ await ctx.init()
13
+
14
+ const oidc = ctx.service<OidcClientService>(DEFAULT_ALIAS)
15
+ const cfg = await oidc.getConfig({ service: GOOGLE_SERVICE })
16
+
17
+ expect(cfg).not.toBeNull()
18
+ expect(cfg!.clientId).toBe('google-client-id-123')
19
+ expect(cfg!.secret).toBe('google-secret-456')
20
+ })
21
+
22
+ test('finds provider by clientId string', async () => {
23
+ const ctx = makeTestContext()
24
+ ctx.configure()
25
+ await ctx.init()
26
+
27
+ const oidc = ctx.service<OidcClientService>(DEFAULT_ALIAS)
28
+ const cfg = await oidc.getConfig('keycloak-admin')
29
+
30
+ expect(cfg).not.toBeNull()
31
+ expect(cfg!.service).toBe('iam-product-api')
32
+ })
33
+
34
+ test('throws when lookup has neither clientId nor service', async () => {
35
+ const ctx = makeTestContext()
36
+ ctx.configure()
37
+ await ctx.init()
38
+
39
+ const oidc = ctx.service<OidcClientService>(DEFAULT_ALIAS)
40
+
41
+ await expect(oidc.getConfig({})).rejects.toBeInstanceOf(AuthManagerError)
42
+ })
43
+
44
+ test('returns undefined for unknown service', async () => {
45
+ const ctx = makeTestContext()
46
+ ctx.configure()
47
+ await ctx.init()
48
+
49
+ const oidc = ctx.service<OidcClientService>(DEFAULT_ALIAS)
50
+ const cfg = await oidc.getConfig({ service: 'nonexistent-service' })
51
+
52
+ expect(cfg).toBeUndefined()
53
+ })
54
+ })
55
+
56
+ describe('@owlmeans/server-oidc-rp — getConfiguration guard', () => {
57
+ test('throws oidc.client.client-id when service-only provider has no clientId on provider or param', async () => {
58
+ const ctx = makeTestContext()
59
+ // Add a provider with no clientId
60
+ ;(ctx.cfg as any).oidc.providers.push({
61
+ service: 'no-client-id-svc',
62
+ secret: 'some-secret',
63
+ basePath: 'realms/test',
64
+ })
65
+ ctx.configure()
66
+ await ctx.init()
67
+
68
+ const oidc = ctx.service<OidcClientService>(DEFAULT_ALIAS)
69
+
70
+ // getConfiguration should throw because neither param.clientId nor cfg.clientId exist
71
+ await expect(
72
+ oidc.getConfiguration({ service: 'no-client-id-svc' })
73
+ ).rejects.toThrow('oidc.client.client-id')
74
+ })
75
+ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.node.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "types": ["bun"],
8
+ "rootDir": "../",
9
+ "noEmit": true
10
+ },
11
+ "include": ["./**/*", "../src/**/*"]
12
+ }
package/tsconfig.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "extends": [
3
- "../tsconfig.default.json",
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.node.json"
4
5
  ],
5
6
  "compilerOptions": {
6
- "rootDir": "./src/", /* Specify the root folder within your source files. */
7
- "outDir": "./build/", /* Specify an output folder for all emitted files. */
8
- "moduleResolution": "Bundler",
7
+ "rootDir": "./src/",
8
+ "outDir": "./build/"
9
9
  },
10
10
  "exclude": [
11
11
  "./dist/**/*",
12
12
  "./build/**/*",
13
+ "./tests/**/*",
13
14
  "./*.ts"
14
15
  ]
15
- }
16
+ }
package/build/.gitkeep DELETED
File without changes