@owlmeans/server-auth 0.1.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.
Files changed (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/build/.gitkeep +0 -0
  4. package/build/actions/index.d.ts +2 -0
  5. package/build/actions/index.d.ts.map +1 -0
  6. package/build/actions/index.js +2 -0
  7. package/build/actions/index.js.map +1 -0
  8. package/build/actions/service.d.ts +4 -0
  9. package/build/actions/service.d.ts.map +1 -0
  10. package/build/actions/service.js +7 -0
  11. package/build/actions/service.js.map +1 -0
  12. package/build/consts.d.ts +5 -0
  13. package/build/consts.d.ts.map +1 -0
  14. package/build/consts.js +5 -0
  15. package/build/consts.js.map +1 -0
  16. package/build/index.d.ts +6 -0
  17. package/build/index.d.ts.map +1 -0
  18. package/build/index.js +5 -0
  19. package/build/index.js.map +1 -0
  20. package/build/manager/actions/index.d.ts +2 -0
  21. package/build/manager/actions/index.d.ts.map +1 -0
  22. package/build/manager/actions/index.js +2 -0
  23. package/build/manager/actions/index.js.map +1 -0
  24. package/build/manager/actions/manager.d.ts +6 -0
  25. package/build/manager/actions/manager.d.ts.map +1 -0
  26. package/build/manager/actions/manager.js +10 -0
  27. package/build/manager/actions/manager.js.map +1 -0
  28. package/build/manager/consts.d.ts +3 -0
  29. package/build/manager/consts.d.ts.map +1 -0
  30. package/build/manager/consts.js +3 -0
  31. package/build/manager/consts.js.map +1 -0
  32. package/build/manager/context.d.ts +3 -0
  33. package/build/manager/context.d.ts.map +1 -0
  34. package/build/manager/context.js +26 -0
  35. package/build/manager/context.js.map +1 -0
  36. package/build/manager/exports.d.ts +16 -0
  37. package/build/manager/exports.d.ts.map +1 -0
  38. package/build/manager/exports.js +13 -0
  39. package/build/manager/exports.js.map +1 -0
  40. package/build/manager/index.d.ts +11 -0
  41. package/build/manager/index.d.ts.map +1 -0
  42. package/build/manager/index.js +10 -0
  43. package/build/manager/index.js.map +1 -0
  44. package/build/manager/main.d.ts +3 -0
  45. package/build/manager/main.d.ts.map +1 -0
  46. package/build/manager/main.js +7 -0
  47. package/build/manager/main.js.map +1 -0
  48. package/build/manager/model.d.ts +3 -0
  49. package/build/manager/model.d.ts.map +1 -0
  50. package/build/manager/model.js +68 -0
  51. package/build/manager/model.js.map +1 -0
  52. package/build/manager/modules.d.ts +2 -0
  53. package/build/manager/modules.d.ts.map +1 -0
  54. package/build/manager/modules.js +21 -0
  55. package/build/manager/modules.js.map +1 -0
  56. package/build/manager/plugins/basic-ed25519.d.ts +4 -0
  57. package/build/manager/plugins/basic-ed25519.d.ts.map +1 -0
  58. package/build/manager/plugins/basic-ed25519.js +42 -0
  59. package/build/manager/plugins/basic-ed25519.js.map +1 -0
  60. package/build/manager/plugins/basic-rely.d.ts +4 -0
  61. package/build/manager/plugins/basic-rely.d.ts.map +1 -0
  62. package/build/manager/plugins/basic-rely.js +152 -0
  63. package/build/manager/plugins/basic-rely.js.map +1 -0
  64. package/build/manager/plugins/export.d.ts +6 -0
  65. package/build/manager/plugins/export.d.ts.map +1 -0
  66. package/build/manager/plugins/export.js +5 -0
  67. package/build/manager/plugins/export.js.map +1 -0
  68. package/build/manager/plugins/index.d.ts +4 -0
  69. package/build/manager/plugins/index.d.ts.map +1 -0
  70. package/build/manager/plugins/index.js +9 -0
  71. package/build/manager/plugins/index.js.map +1 -0
  72. package/build/manager/plugins/re-captcha.d.ts +4 -0
  73. package/build/manager/plugins/re-captcha.d.ts.map +1 -0
  74. package/build/manager/plugins/re-captcha.js +49 -0
  75. package/build/manager/plugins/re-captcha.js.map +1 -0
  76. package/build/manager/plugins/types.d.ts +25 -0
  77. package/build/manager/plugins/types.d.ts.map +1 -0
  78. package/build/manager/plugins/types.js +2 -0
  79. package/build/manager/plugins/types.js.map +1 -0
  80. package/build/manager/plugins/utils.d.ts +8 -0
  81. package/build/manager/plugins/utils.d.ts.map +1 -0
  82. package/build/manager/plugins/utils.js +18 -0
  83. package/build/manager/plugins/utils.js.map +1 -0
  84. package/build/manager/rely/flow.d.ts +5 -0
  85. package/build/manager/rely/flow.d.ts.map +1 -0
  86. package/build/manager/rely/flow.js +127 -0
  87. package/build/manager/rely/flow.js.map +1 -0
  88. package/build/manager/rely/index.d.ts +2 -0
  89. package/build/manager/rely/index.d.ts.map +1 -0
  90. package/build/manager/rely/index.js +2 -0
  91. package/build/manager/rely/index.js.map +1 -0
  92. package/build/manager/rely.d.ts +3 -0
  93. package/build/manager/rely.d.ts.map +1 -0
  94. package/build/manager/rely.js +63 -0
  95. package/build/manager/rely.js.map +1 -0
  96. package/build/manager/types.d.ts +33 -0
  97. package/build/manager/types.d.ts.map +1 -0
  98. package/build/manager/types.js +2 -0
  99. package/build/manager/types.js.map +1 -0
  100. package/build/manager/utils/index.d.ts +2 -0
  101. package/build/manager/utils/index.d.ts.map +1 -0
  102. package/build/manager/utils/index.js +2 -0
  103. package/build/manager/utils/index.js.map +1 -0
  104. package/build/manager/utils/trusted.d.ts +5 -0
  105. package/build/manager/utils/trusted.d.ts.map +1 -0
  106. package/build/manager/utils/trusted.js +11 -0
  107. package/build/manager/utils/trusted.js.map +1 -0
  108. package/build/model/index.d.ts +2 -0
  109. package/build/model/index.d.ts.map +1 -0
  110. package/build/model/index.js +2 -0
  111. package/build/model/index.js.map +1 -0
  112. package/build/model/rely/consumer.d.ts +3 -0
  113. package/build/model/rely/consumer.d.ts.map +1 -0
  114. package/build/model/rely/consumer.js +9 -0
  115. package/build/model/rely/consumer.js.map +1 -0
  116. package/build/model/rely/index.d.ts +5 -0
  117. package/build/model/rely/index.d.ts.map +1 -0
  118. package/build/model/rely/index.js +4 -0
  119. package/build/model/rely/index.js.map +1 -0
  120. package/build/model/rely/peer.d.ts +5 -0
  121. package/build/model/rely/peer.d.ts.map +1 -0
  122. package/build/model/rely/peer.js +36 -0
  123. package/build/model/rely/peer.js.map +1 -0
  124. package/build/model/rely/provider.d.ts +3 -0
  125. package/build/model/rely/provider.d.ts.map +1 -0
  126. package/build/model/rely/provider.js +6 -0
  127. package/build/model/rely/provider.js.map +1 -0
  128. package/build/model/rely/types.d.ts +8 -0
  129. package/build/model/rely/types.d.ts.map +1 -0
  130. package/build/model/rely/types.js +2 -0
  131. package/build/model/rely/types.js.map +1 -0
  132. package/build/modules.d.ts +3 -0
  133. package/build/modules.d.ts.map +1 -0
  134. package/build/modules.js +11 -0
  135. package/build/modules.js.map +1 -0
  136. package/build/service.d.ts +7 -0
  137. package/build/service.d.ts.map +1 -0
  138. package/build/service.js +90 -0
  139. package/build/service.js.map +1 -0
  140. package/build/types.d.ts +13 -0
  141. package/build/types.d.ts.map +1 -0
  142. package/build/types.js +2 -0
  143. package/build/types.js.map +1 -0
  144. package/package.json +76 -0
  145. package/src/actions/index.ts +2 -0
  146. package/src/actions/service.ts +16 -0
  147. package/src/consts.ts +8 -0
  148. package/src/index.ts +6 -0
  149. package/src/manager/README.md +507 -0
  150. package/src/manager/actions/index.ts +2 -0
  151. package/src/manager/actions/manager.ts +23 -0
  152. package/src/manager/consts.ts +4 -0
  153. package/src/manager/context.ts +33 -0
  154. package/src/manager/exports.ts +17 -0
  155. package/src/manager/index.ts +11 -0
  156. package/src/manager/main.ts +9 -0
  157. package/src/manager/model.ts +93 -0
  158. package/src/manager/modules.ts +29 -0
  159. package/src/manager/plugins/basic-ed25519.ts +56 -0
  160. package/src/manager/plugins/basic-rely.ts +167 -0
  161. package/src/manager/plugins/export.ts +6 -0
  162. package/src/manager/plugins/index.ts +13 -0
  163. package/src/manager/plugins/re-captcha.ts +62 -0
  164. package/src/manager/plugins/types.ts +27 -0
  165. package/src/manager/plugins/utils.ts +21 -0
  166. package/src/manager/rely/flow.ts +135 -0
  167. package/src/manager/rely/index.ts +2 -0
  168. package/src/manager/rely.ts +90 -0
  169. package/src/manager/types.ts +42 -0
  170. package/src/manager/utils/index.ts +2 -0
  171. package/src/manager/utils/trusted.ts +16 -0
  172. package/src/model/index.ts +2 -0
  173. package/src/model/rely/consumer.ts +11 -0
  174. package/src/model/rely/index.ts +5 -0
  175. package/src/model/rely/peer.ts +43 -0
  176. package/src/model/rely/provider.ts +9 -0
  177. package/src/model/rely/types.ts +8 -0
  178. package/src/modules.ts +17 -0
  179. package/src/service.ts +126 -0
  180. package/src/types.ts +15 -0
  181. package/tsconfig.json +15 -0
  182. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,93 @@
1
+ import type { AuthModel, AppConfig, AppContext } from './types.js'
2
+ import { AuthenFailed, AuthenPayloadError } from '@owlmeans/auth'
3
+ import type { AuthCredentials } from '@owlmeans/auth'
4
+ import { getPlugin } from './plugins/utils.js'
5
+ import { AUTH_CACHE, AUTHEN_TIMEFRAME } from '../consts.js'
6
+ import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
7
+ import type { EnvelopeModel } from '@owlmeans/basic-envelope'
8
+ import { createRelyFlow } from './rely/flow.js'
9
+ import { trusted } from './utils/trusted.js'
10
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
11
+ import type { Resource } from '@owlmeans/resource'
12
+ import type { AuthSpent } from '../types.js'
13
+
14
+ type Config = ServerConfig
15
+ type Context = ServerContext<Config>
16
+
17
+ export const makeAuthModel = (context: AppContext<AppConfig>): AuthModel => {
18
+ const cache = (context: Context): Resource<AuthSpent> =>
19
+ context.resource<Resource<AuthSpent>>(AUTH_CACHE)
20
+
21
+ const model: AuthModel = {
22
+ init: async request => {
23
+ const plugin = getPlugin(request.type, context)
24
+ const response = await plugin.init(request)
25
+
26
+ const envelope = makeEnvelopeModel(plugin.type)
27
+
28
+ /**
29
+ * @TODO It looks like we don't validate sourced challenge by any means
30
+ * So we need to:
31
+ * 1. Remove it cause we process it as a separate property anyway
32
+ * 2. Fix it because it looks like this separate property isn't validated
33
+ */
34
+ const challengeToken = request.source != null
35
+ ? `${request.source}:${response.challenge}` : response.challenge
36
+
37
+ const [, keyPair] = await trusted(context)
38
+ const challenge = await envelope.send(challengeToken, AUTHEN_TIMEFRAME).sign(keyPair, EnvelopeKind.Wrap)
39
+
40
+ return { challenge }
41
+ },
42
+
43
+ authenticate: async credential => {
44
+ const [trustedUser, keyPair] = await trusted(context)
45
+ const envelope: EnvelopeModel = makeEnvelopeModel(credential.challenge, EnvelopeKind.Wrap)
46
+ if (!await envelope.verify(keyPair)) {
47
+ throw new AuthenFailed('challenge')
48
+ }
49
+
50
+ const msg: string = envelope.message(true)
51
+
52
+ try {
53
+ await cache(context).create({ id: msg }, { ttl: (envelope.envelope.ttl ?? AUTHEN_TIMEFRAME) / 1000 })
54
+ } catch (e) {
55
+ const error = new AuthenFailed('challenge')
56
+ if (e instanceof Error) {
57
+ error.oiriginalStack = `${e} : ${e.stack}`
58
+ }
59
+ throw error
60
+ }
61
+
62
+ if (credential.userId == null) {
63
+ throw new AuthenPayloadError('userId')
64
+ }
65
+
66
+ const plugin = getPlugin(envelope.type(), context)
67
+
68
+ // @TODO this token PROBABLY needs to be registered somewhere and rechecked
69
+ // from time to time to make sure that the session is not deleted
70
+ // or permission hasn't changed.
71
+ // Alternative solution is to have permissions cached and cleaned
72
+ // when some broadcast message is sent from authentication system.
73
+
74
+ const challenge = credential.challenge
75
+ const { token } = await plugin.authenticate(Object.assign(credential, { challenge: msg }))
76
+ credential.challenge = token === '' ? challenge : token
77
+
78
+ credential.credential = trustedUser.id
79
+
80
+ return {
81
+ token: await makeEnvelopeModel<AuthCredentials>(credential.type)
82
+ .send(credential, AUTHEN_TIMEFRAME)
83
+ .sign(keyPair, EnvelopeKind.Token)
84
+ }
85
+ },
86
+
87
+ rely: async (conn, auth) => {
88
+ conn.authenticate = createRelyFlow(context, conn, auth)
89
+ }
90
+ }
91
+
92
+ return model
93
+ }
@@ -0,0 +1,29 @@
1
+
2
+ import { AUTHEN, AUTHEN_AUTHEN, AUTHEN_INIT, AUTHEN_RELY, MOD_RECAPTCHA } from '@owlmeans/auth'
3
+ import { modules as list } from '@owlmeans/auth-common'
4
+ import { modules as apiConfig } from '@owlmeans/api-config-server'
5
+ import { elevate, guard } from '@owlmeans/server-module'
6
+ import * as actions from './actions/index.js'
7
+ import { module } from '@owlmeans/client-module'
8
+ import { backend, route, RouteMethod } from '@owlmeans/route'
9
+ import { handleIntermediate } from '@owlmeans/server-api'
10
+ import { DEFAULT_RELY } from './consts.js'
11
+
12
+ elevate(list, AUTHEN, handleIntermediate(
13
+ async (_, context) => await context.updateContext() as typeof context
14
+ ), { intermediate: true })
15
+ elevate(list, AUTHEN_INIT, actions.authenticationInit)
16
+ elevate(list, AUTHEN_AUTHEN, actions.authenticate)
17
+ elevate(list, AUTHEN_RELY, actions.rely, guard(DEFAULT_RELY))
18
+
19
+ export const modules = list
20
+ list.push(...apiConfig)
21
+ list.push(
22
+ module(
23
+ route(MOD_RECAPTCHA, '/api/siteverify', backend({
24
+ host: 'https://www.google.com',
25
+ base: 'recaptcha',
26
+ secure: true,
27
+ }, RouteMethod.POST))
28
+ )
29
+ )
@@ -0,0 +1,56 @@
1
+ import { AuthenticationType, AuthenFailed, ALL_SCOPES, AuthRole } from '@owlmeans/auth'
2
+ import type { AuthPlugin } from './types.js'
3
+ import { assertType } from './utils.js'
4
+ import { randomBytes } from '@noble/hashes/utils'
5
+ import { base64 } from '@scure/base'
6
+ import { fromPubKey } from '@owlmeans/basic-keys'
7
+ import type { AppConfig, AppContext } from '../types.js'
8
+ import { TRUSTED } from '@owlmeans/config'
9
+ import type { TrustedRecord } from '@owlmeans/auth-common'
10
+
11
+ export const basicEd25519 = <C extends AppConfig, T extends AppContext<C>>(context: T): AuthPlugin => {
12
+ const plugin: AuthPlugin = {
13
+ type: AuthenticationType.BasicEd25519,
14
+ init: async request => {
15
+ assertType(request.type, plugin)
16
+
17
+ const challenge = base64.encode(randomBytes(32))
18
+
19
+ return { challenge }
20
+ },
21
+
22
+ authenticate: async credential => {
23
+ /**
24
+ * @TODO We need to find users in some db not just in static
25
+ */
26
+ const systemUser = await context.getConfigResource(TRUSTED)
27
+ .load<TrustedRecord>(credential.userId)
28
+
29
+ if (systemUser == null) {
30
+ throw new AuthenFailed()
31
+ }
32
+
33
+ if (systemUser.credential == null) {
34
+ throw new SyntaxError('System user missconfigured')
35
+ }
36
+
37
+ const key = fromPubKey(systemUser.credential)
38
+
39
+ if (!await key.verify(credential.challenge, credential.credential)) {
40
+ throw new AuthenFailed(`challenge:${plugin.type}`)
41
+ }
42
+
43
+ const token = base64.encode(randomBytes(32))
44
+
45
+ // @TODO we need to do something with scopes - it's not secure
46
+ credential.scopes = [ALL_SCOPES]
47
+ credential.role = AuthRole.Superuser
48
+ credential.challenge = token
49
+ credential.type = AuthenticationType.OneTimeToken
50
+
51
+ return { token }
52
+ }
53
+ }
54
+
55
+ return plugin
56
+ }
@@ -0,0 +1,167 @@
1
+ import { AuthenFailed, AuthenPayloadError, AuthenticationType, AuthManagerError, AuthorizationError } from '@owlmeans/auth'
2
+ import type { AllowanceResponse, Auth, RelyToken } from '@owlmeans/auth'
3
+ import type { AppContext, RelyAllowanceRequest, RelyCarrier } from '../types.js'
4
+ import type { AuthPlugin, AuthRedisResource, RelyRecord } from './types.js'
5
+ import { makeConsumerRely, makeProviderRely } from '../../model/index.js'
6
+ import { trusted } from '../utils/trusted.js'
7
+ import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
8
+ import { AUTH_CACHE } from '../../consts.js'
9
+ import { ResourceError } from '@owlmeans/resource'
10
+ import { RELY_CALL_TIMEOUT, RELY_PIN_PERFIX, RELY_TOKEN_PREFIX } from '@owlmeans/auth-common'
11
+
12
+ const _subscriptions: Record<string, CallableFunction> = {}
13
+ export const basicRely = (context: AppContext, type?: string): AuthPlugin => {
14
+ const plugin: AuthPlugin = {
15
+ type: type ?? AuthenticationType.RelyHandshake,
16
+
17
+ // This method is used by consumer and by provider (wallet)
18
+ // to initialize shared authentication process.
19
+ // The idea is that both subscribe to wait for a handshake value
20
+ // from the peer, that in fact is a subscription to a redis message tunnel.
21
+ init: async (request: RelyAllowanceRequest): Promise<AllowanceResponse> => {
22
+ const [, keyPair] = await trusted(context)
23
+ const tunnel = context.resource<AuthRedisResource>(AUTH_CACHE)
24
+ let relyMsg: RelyToken
25
+ do {
26
+ try {
27
+ const rely = request.auth == null ? makeConsumerRely() : makeProviderRely()
28
+
29
+ relyMsg = rely.message()
30
+ const pin = relyMsg.pin != null ? `${RELY_PIN_PERFIX}${relyMsg.pin}` : null
31
+ const token = relyMsg.token != null ? `${RELY_TOKEN_PREFIX}${relyMsg.token}` : null
32
+
33
+ const response = { challenge: await rely.sign(keyPair, EnvelopeKind.Wrap) }
34
+
35
+ if (pin != null) {
36
+ await tunnel.create({ ...response, id: pin }, { ttl: RELY_CALL_TIMEOUT })
37
+ }
38
+ if (token != null) {
39
+ await tunnel.create({ ...response, id: token }, { ttl: RELY_CALL_TIMEOUT })
40
+ }
41
+
42
+ if (request.provideRely != null) {
43
+ await Promise.all([pin, token].filter(key => key != null).map(async key => {
44
+ _subscriptions[key] = await tunnel.subscribe(async value => {
45
+ if (value.challenge != null) {
46
+ const rely = makeEnvelopeModel<RelyToken>(value.challenge, EnvelopeKind.Wrap)
47
+ if (await rely.verify(keyPair)) {
48
+ [pin, token].filter(key => key != null).forEach(key => {
49
+ _subscriptions[key]?.()
50
+ if (_subscriptions[key] != null) {
51
+ delete _subscriptions[key]
52
+ }
53
+ })
54
+ await request.provideRely?.(rely.message(), relyMsg, true)
55
+ }
56
+ }
57
+ }, { /*ttl: RELY_CALL_TIMEOUT,*/ key, once: true })
58
+
59
+ // @TODO they need to be killed on close - but actually live a little bit longer
60
+ // then timeout for them is ok
61
+ setTimeout(() => {
62
+ if (_subscriptions[key] != null) {
63
+ try {
64
+ void _subscriptions[key]()
65
+ delete _subscriptions[key]
66
+ } finally {
67
+ void request.conn?.close()
68
+ }
69
+ }
70
+ }, RELY_CALL_TIMEOUT * 1000)
71
+ }))
72
+ }
73
+
74
+ return response
75
+ } catch (e) {
76
+ if (e instanceof ResourceError) {
77
+ console.error('COLISION ON TRY TO CREATE RELY PIN\TOKEN...', e)
78
+ } else {
79
+ throw e
80
+ }
81
+ }
82
+ await new Promise(resolve => setTimeout(resolve, 500))
83
+ } while (true)
84
+ },
85
+
86
+ // This part of flow is entered by the party that is active (provides peer's code - token or pin)
87
+ authenticate: async credential => {
88
+ const [, keyPair] = await trusted(context)
89
+ const tunnel = context.resource<AuthRedisResource>(AUTH_CACHE)
90
+
91
+ // 1. Just get a peer connection based on provided credential
92
+ // 2. Provision it with provided own data
93
+ // 3. Delete own rely connection seeds
94
+
95
+ let rely: RelyToken
96
+ try {
97
+ let peer: RelyRecord
98
+ if (credential.credential.startsWith(RELY_PIN_PERFIX)) {
99
+ const key = credential.credential.substring(RELY_PIN_PERFIX.length)
100
+ if (key.length > 12) {
101
+ throw new AuthenPayloadError('pin')
102
+ }
103
+ peer = await tunnel.get(credential.credential)
104
+ } else if (credential.credential.startsWith(RELY_TOKEN_PREFIX)) {
105
+ const key = credential.credential.substring(RELY_TOKEN_PREFIX.length)
106
+ if (key.length > 16) {
107
+ throw new AuthenPayloadError('token')
108
+ }
109
+ peer = await tunnel.get(credential.credential)
110
+ } else {
111
+ throw new AuthenPayloadError('credential')
112
+ }
113
+ const envelop = makeEnvelopeModel<RelyToken>(peer.challenge, EnvelopeKind.Wrap)
114
+ if (!envelop.verify(keyPair)) {
115
+ throw new AuthManagerError()
116
+ }
117
+ rely = envelop.message()
118
+ } catch (e) {
119
+ if (e instanceof ResourceError) {
120
+ console.error(e)
121
+ throw new AuthorizationError('rely')
122
+ } else {
123
+ throw e
124
+ }
125
+ }
126
+
127
+ const myChallenge = makeEnvelopeModel<RelyToken>(credential.challenge, EnvelopeKind.Wrap)
128
+ if (!myChallenge.verify(keyPair)) {
129
+ throw new AuthenFailed()
130
+ }
131
+
132
+ await tunnel.update({ id: credential.credential, challenge: myChallenge.wrap() })
133
+
134
+ const myRely = myChallenge.message();
135
+
136
+ [
137
+ myRely.pin != null ? `${RELY_PIN_PERFIX}${myRely.pin}` : null,
138
+ myRely.token != null ? `${RELY_TOKEN_PREFIX}${myRely.token}` : null
139
+ ].filter(key => key != null).forEach(key => {
140
+ if (_subscriptions[key] != null) {
141
+ void _subscriptions[key]()
142
+ delete _subscriptions[key]
143
+ }
144
+ })
145
+
146
+ const token = makeEnvelopeModel<RelyCarrier>(AuthenticationType.RelyHandshake)
147
+ token.send({ source: myRely, rely })
148
+
149
+ credential.type = AuthenticationType.RelyHandshake
150
+
151
+ // @TODO think about it: we create a mixed object here that is credentials and authentication
152
+ // in the same time. Actually it's a bit dirty. And it means that auth service at the end of the
153
+ // day can produce both (authentication token and authentication token "ticket").
154
+ // Why we do it this way here: cause the consumer of authentication is the auth service itself
155
+ // and there is no need to "exchange" the token (what we do when we come via url redirect to another service
156
+ // with the one-time auth token potentially exposed as a result).
157
+ const _auth: Auth = credential as unknown as Auth
158
+ _auth.createdAt = new Date()
159
+ _auth.token = token.tokenize()
160
+ _auth.isUser = true
161
+
162
+ return { token: _auth.token }
163
+ }
164
+ }
165
+
166
+ return plugin
167
+ }
@@ -0,0 +1,6 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './basic-ed25519.js'
4
+ export * from './re-captcha.js'
5
+ export * from './basic-rely.js'
6
+ export * from './utils.js'
@@ -0,0 +1,13 @@
1
+ import { AuthenticationType } from '@owlmeans/auth'
2
+ import type { AuthPlugin } from './types.js'
3
+ import { basicEd25519 } from './basic-ed25519.js'
4
+ import type { AppContext, AppConfig } from '../types.js'
5
+ import { reCaptcha } from './re-captcha.js'
6
+ import { basicRely } from './basic-rely.js'
7
+
8
+ export const plugins: Record<string, <C extends AppConfig, T extends AppContext<C>>(context: T) => AuthPlugin> = {}
9
+
10
+ plugins[AuthenticationType.BasicEd25519] = basicEd25519 as typeof plugins[keyof typeof plugins]
11
+ plugins[AuthenticationType.ReCaptcha] = reCaptcha as typeof plugins[keyof typeof plugins]
12
+ plugins[AuthenticationType.RelyHandshake] = basicRely as typeof plugins[keyof typeof plugins]
13
+
@@ -0,0 +1,62 @@
1
+ import { AuthenFailed, AuthenticationType, AuthPluginError, MOD_RECAPTCHA, AUTH_SCOPE, AuthRole } from '@owlmeans/auth'
2
+ import type { AppContext, AppConfig } from '../types.js'
3
+ import type { AuthPlugin, RecaptchaRequest, RecpatchaResponse } from './types.js'
4
+ import { PLUGINS } from '@owlmeans/config'
5
+ import type { PluginConfig } from '@owlmeans/config'
6
+ import { PluginMissconfigured } from '@owlmeans/config'
7
+ import type { ClientModule } from '@owlmeans/client-module'
8
+ import { ModuleOutcome } from '@owlmeans/module'
9
+ import { base64 } from '@scure/base'
10
+ import { randomBytes } from '@noble/hashes/utils'
11
+ import { assertType } from './utils.js'
12
+
13
+ export const reCaptcha = <C extends AppConfig, T extends AppContext<C>>(context: T): AuthPlugin => {
14
+ const plugin: AuthPlugin = {
15
+ type: AuthenticationType.ReCaptcha,
16
+
17
+ init: async request => {
18
+ assertType(request.type, plugin)
19
+
20
+ const challenge = base64.encode(randomBytes(32))
21
+
22
+ return { challenge }
23
+ },
24
+
25
+ authenticate: async credential => {
26
+ const cfg = context.getConfigResource(PLUGINS)
27
+ const config = await cfg.get<PluginConfig>(MOD_RECAPTCHA)
28
+ if (config.value == null) {
29
+ throw new PluginMissconfigured('value')
30
+ }
31
+ const validateRecaptcha = context.module<ClientModule<RecpatchaResponse>>(MOD_RECAPTCHA)
32
+ const [result, status] = await validateRecaptcha.call<RecpatchaResponse, RecaptchaRequest>({
33
+ body: {
34
+ secret: config.value,
35
+ response: credential.credential
36
+ },
37
+ headers: { 'content-type': 'application/x-www-form-urlencoded' }
38
+ })
39
+ if (status !== ModuleOutcome.Ok) {
40
+ throw new AuthPluginError('recaptcha:api')
41
+ }
42
+ if (!result.success) {
43
+ throw new AuthenFailed('recaptcha:' + (result['error-codes']?.join(',') ?? 'unknown'))
44
+ }
45
+
46
+ credential.scopes = [AUTH_SCOPE]
47
+ credential.role = AuthRole.Guest
48
+ credential.type = AuthenticationType.ReCaptcha
49
+
50
+ const [previous, challenge] = credential.challenge.split(':') as [string, string | undefined]
51
+ if (challenge != null) {
52
+ credential.challenge = previous
53
+
54
+ return { token: previous }
55
+ }
56
+
57
+ return { token: '' }
58
+ }
59
+ }
60
+
61
+ return plugin
62
+ }
@@ -0,0 +1,27 @@
1
+ import type { AllowanceResponse } from '@owlmeans/auth'
2
+ import type { AuthModel } from '../types.js'
3
+ import type { AbstractRequest } from '@owlmeans/module'
4
+ import type { RedisResource } from '@owlmeans/redis-resource'
5
+ import type { ResourceRecord } from '@owlmeans/resource'
6
+
7
+ export interface AuthPlugin extends Omit<AuthModel, "rely"> {
8
+ type: string
9
+ }
10
+
11
+ export interface RecpatchaResponse {
12
+ success: boolean
13
+ challenge_ts: number
14
+ hostname: string
15
+ 'error-codes'?: string[]
16
+ }
17
+
18
+ export interface RecaptchaRequest extends AbstractRequest<{
19
+ secret: string
20
+ response: string
21
+ remoteip?: string
22
+ }> { }
23
+
24
+ export interface RelyRecord extends ResourceRecord, AllowanceResponse {
25
+ }
26
+
27
+ export interface AuthRedisResource extends RedisResource<RelyRecord> {}
@@ -0,0 +1,21 @@
1
+ import { AuthUnknown, TypeMissmatchError } from '@owlmeans/auth'
2
+ import type { AuthPlugin } from './types.js'
3
+ import { plugins } from './index.js'
4
+ import type { AppContext, AppConfig } from '../types.js'
5
+
6
+ export const assertType = (type: string, plugin: AuthPlugin) => {
7
+ if (type !== plugin.type) {
8
+ throw new TypeMissmatchError(plugin.type)
9
+ }
10
+ }
11
+
12
+ /**
13
+ * @throws {AuthUnknown}
14
+ */
15
+ export const getPlugin = (type: string, context: AppContext<AppConfig>): AuthPlugin => {
16
+ const plugin = plugins[type]
17
+ if (plugin == null) {
18
+ throw new AuthUnknown(type)
19
+ }
20
+ return plugins[type](context)
21
+ }
@@ -0,0 +1,135 @@
1
+ import type { AllowanceRequest, Auth, AuthCredentials, RelyToken } from '@owlmeans/auth'
2
+ import { AUTH_SCOPE, AuthenticationStage, AuthenticationType, AuthPluginError, AuthRole, RELY_3RD } from '@owlmeans/auth'
3
+ import type { AuthenticateMethod, Connection, EventMessage, Message } from '@owlmeans/socket'
4
+ import { isEventMessage, isMessage } from '@owlmeans/socket'
5
+ import type { AppContext, RelyAllowanceRequest, RelyLinker, RelyCarrier } from '../types.js'
6
+ import { makeAuthModel } from '../model.js'
7
+ import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
8
+ import { trusted } from '../utils/trusted.js'
9
+ import { RELY_TUNNEL } from '../consts.js'
10
+ import type { RedisResource } from '@owlmeans/redis-resource'
11
+ import { RELY_ACTION_TIMEOUT } from '@owlmeans/auth-common'
12
+
13
+ // 1. There is a difference between privileged (provider)
14
+ // and non-privileged (consumer) request
15
+ // 2. Source allows to distinguish between provider and consumer
16
+ // but probably it needs to be universalized some way
17
+ // cause potentially wallet can also be consumer
18
+ // (right now consumer will request authentication type)
19
+ // 3. General idea is that: when you are connected, the auth
20
+ // process can be started from scratch following same stages
21
+ // and using the same plugins but via socket
22
+
23
+ export const createRelyFlow = (context: AppContext, conn: Connection, auth?: Auth | null): AuthenticateMethod => {
24
+ const auhtenticate = conn.authenticate
25
+
26
+ const model = makeAuthModel(context)
27
+ let internalAuth: Auth | null = auth ?? null
28
+
29
+ const linker: RelyLinker = async (rely, source, notify) => {
30
+ const tunnel = context.resource<RedisResource<Message<any>>>(RELY_TUNNEL)
31
+ const closeReceiver = await tunnel.subscribe(async message => {
32
+ if (isMessage(message, true)) {
33
+ await conn.send(message)
34
+ } else if (isEventMessage(message, true)) {
35
+ if ((message as EventMessage<unknown>).event === 'close') {
36
+ await conn.close()
37
+ closeSender()
38
+ await closeReceiver()
39
+ }
40
+ }
41
+ }, source.nonce)
42
+ // We allow to just forward call messages back and forth
43
+ conn._receiveCall = async msg => { console.info(source.nonce, 'Forward call', msg.method, msg.id) }
44
+ conn._receiveResult = async msg => { console.info(source.nonce, 'Forward result', msg.id) }
45
+ conn._receiveError = async msg => { console.info(source.nonce, 'Forward error', msg.id) }
46
+ conn.defaultCallTimeout = RELY_ACTION_TIMEOUT * 1000
47
+ const closeSender = conn.listen(async message => {
48
+ if (isMessage(message, true)) {
49
+ const forward = { ...message }
50
+ if (forward.rawData != null) {
51
+ delete forward.rawData
52
+ }
53
+ await tunnel.publish(forward, rely.nonce)
54
+ } else if (isEventMessage(message, true)) {
55
+ if (message.event === 'close') {
56
+ const forward = { ...message as Message<unknown> }
57
+ if (forward.rawData != null) {
58
+ delete forward.rawData
59
+ }
60
+ await tunnel.publish(forward, rely.nonce)
61
+ closeSender()
62
+ await closeReceiver()
63
+ }
64
+ }
65
+ })
66
+ if (notify === true) {
67
+ internalAuth = {
68
+ token: makeEnvelopeModel<RelyToken>(RELY_3RD).send(rely).tokenize(),
69
+ isUser: true,
70
+ createdAt: new Date(),
71
+ type: AuthenticationType.RelyHandshake,
72
+ role: AuthRole.User,
73
+ userId: source.nonce,
74
+ scopes: [AUTH_SCOPE]
75
+ }
76
+ await conn.auth(AuthenticationStage.Authenticated, internalAuth)
77
+ }
78
+ }
79
+
80
+ conn.authenticate = async (stage, payload) => {
81
+ const [, keyPair] = await trusted(context)
82
+ switch (stage) {
83
+ // This part of flow is entered by both consumer and provider (wallet)
84
+ case AuthenticationStage.Init:
85
+ const _payload = payload as RelyAllowanceRequest
86
+ if (auth != null) {
87
+ _payload.auth = auth
88
+ } else if (_payload.auth != null) {
89
+ delete _payload.auth
90
+ }
91
+ _payload.provideRely = linker
92
+ _payload.conn = conn
93
+
94
+ try {
95
+ return [stage, await model.init(_payload as AllowanceRequest) as any]
96
+ } catch (e) {
97
+ console.error(e)
98
+ await conn.close()
99
+ }
100
+ break
101
+ // This part of flow is entered by that actor who enters peer's key (token or pin)
102
+ case AuthenticationStage.Authenticate:
103
+ try {
104
+ const cred = payload as AuthCredentials
105
+ const authenticated = await model.authenticate(cred)
106
+ const envelope = makeEnvelopeModel<Auth>(authenticated.token, EnvelopeKind.Token)
107
+ if (!await envelope.verify(keyPair)) {
108
+ throw new AuthPluginError('internal-rely')
109
+ }
110
+ if (envelope.type() !== AuthenticationType.RelyHandshake) {
111
+ throw new AuthPluginError('internal-rely')
112
+ }
113
+ internalAuth = envelope.message()
114
+
115
+ const token = makeEnvelopeModel<RelyCarrier>(internalAuth.token, EnvelopeKind.Token).message()
116
+
117
+ await linker(token.rely, token.source)
118
+
119
+ return [AuthenticationStage.Authenticated, internalAuth as any]
120
+ } catch (e) {
121
+ console.error(e)
122
+ await conn.close()
123
+ }
124
+ break
125
+ // Support intermediate stage for visual confirmation
126
+ case AuthenticationStage.Authenticated:
127
+ return [stage, internalAuth as any]
128
+ default:
129
+ return auhtenticate(stage, payload)
130
+ }
131
+ return [stage, null as any]
132
+ }
133
+
134
+ return conn.authenticate
135
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export * from './flow.js'