@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.
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/build/.gitkeep +0 -0
- package/build/actions/index.d.ts +2 -0
- package/build/actions/index.d.ts.map +1 -0
- package/build/actions/index.js +2 -0
- package/build/actions/index.js.map +1 -0
- package/build/actions/service.d.ts +4 -0
- package/build/actions/service.d.ts.map +1 -0
- package/build/actions/service.js +7 -0
- package/build/actions/service.js.map +1 -0
- package/build/consts.d.ts +5 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +5 -0
- package/build/consts.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/manager/actions/index.d.ts +2 -0
- package/build/manager/actions/index.d.ts.map +1 -0
- package/build/manager/actions/index.js +2 -0
- package/build/manager/actions/index.js.map +1 -0
- package/build/manager/actions/manager.d.ts +6 -0
- package/build/manager/actions/manager.d.ts.map +1 -0
- package/build/manager/actions/manager.js +10 -0
- package/build/manager/actions/manager.js.map +1 -0
- package/build/manager/consts.d.ts +3 -0
- package/build/manager/consts.d.ts.map +1 -0
- package/build/manager/consts.js +3 -0
- package/build/manager/consts.js.map +1 -0
- package/build/manager/context.d.ts +3 -0
- package/build/manager/context.d.ts.map +1 -0
- package/build/manager/context.js +26 -0
- package/build/manager/context.js.map +1 -0
- package/build/manager/exports.d.ts +16 -0
- package/build/manager/exports.d.ts.map +1 -0
- package/build/manager/exports.js +13 -0
- package/build/manager/exports.js.map +1 -0
- package/build/manager/index.d.ts +11 -0
- package/build/manager/index.d.ts.map +1 -0
- package/build/manager/index.js +10 -0
- package/build/manager/index.js.map +1 -0
- package/build/manager/main.d.ts +3 -0
- package/build/manager/main.d.ts.map +1 -0
- package/build/manager/main.js +7 -0
- package/build/manager/main.js.map +1 -0
- package/build/manager/model.d.ts +3 -0
- package/build/manager/model.d.ts.map +1 -0
- package/build/manager/model.js +68 -0
- package/build/manager/model.js.map +1 -0
- package/build/manager/modules.d.ts +2 -0
- package/build/manager/modules.d.ts.map +1 -0
- package/build/manager/modules.js +21 -0
- package/build/manager/modules.js.map +1 -0
- package/build/manager/plugins/basic-ed25519.d.ts +4 -0
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -0
- package/build/manager/plugins/basic-ed25519.js +42 -0
- package/build/manager/plugins/basic-ed25519.js.map +1 -0
- package/build/manager/plugins/basic-rely.d.ts +4 -0
- package/build/manager/plugins/basic-rely.d.ts.map +1 -0
- package/build/manager/plugins/basic-rely.js +152 -0
- package/build/manager/plugins/basic-rely.js.map +1 -0
- package/build/manager/plugins/export.d.ts +6 -0
- package/build/manager/plugins/export.d.ts.map +1 -0
- package/build/manager/plugins/export.js +5 -0
- package/build/manager/plugins/export.js.map +1 -0
- package/build/manager/plugins/index.d.ts +4 -0
- package/build/manager/plugins/index.d.ts.map +1 -0
- package/build/manager/plugins/index.js +9 -0
- package/build/manager/plugins/index.js.map +1 -0
- package/build/manager/plugins/re-captcha.d.ts +4 -0
- package/build/manager/plugins/re-captcha.d.ts.map +1 -0
- package/build/manager/plugins/re-captcha.js +49 -0
- package/build/manager/plugins/re-captcha.js.map +1 -0
- package/build/manager/plugins/types.d.ts +25 -0
- package/build/manager/plugins/types.d.ts.map +1 -0
- package/build/manager/plugins/types.js +2 -0
- package/build/manager/plugins/types.js.map +1 -0
- package/build/manager/plugins/utils.d.ts +8 -0
- package/build/manager/plugins/utils.d.ts.map +1 -0
- package/build/manager/plugins/utils.js +18 -0
- package/build/manager/plugins/utils.js.map +1 -0
- package/build/manager/rely/flow.d.ts +5 -0
- package/build/manager/rely/flow.d.ts.map +1 -0
- package/build/manager/rely/flow.js +127 -0
- package/build/manager/rely/flow.js.map +1 -0
- package/build/manager/rely/index.d.ts +2 -0
- package/build/manager/rely/index.d.ts.map +1 -0
- package/build/manager/rely/index.js +2 -0
- package/build/manager/rely/index.js.map +1 -0
- package/build/manager/rely.d.ts +3 -0
- package/build/manager/rely.d.ts.map +1 -0
- package/build/manager/rely.js +63 -0
- package/build/manager/rely.js.map +1 -0
- package/build/manager/types.d.ts +33 -0
- package/build/manager/types.d.ts.map +1 -0
- package/build/manager/types.js +2 -0
- package/build/manager/types.js.map +1 -0
- package/build/manager/utils/index.d.ts +2 -0
- package/build/manager/utils/index.d.ts.map +1 -0
- package/build/manager/utils/index.js +2 -0
- package/build/manager/utils/index.js.map +1 -0
- package/build/manager/utils/trusted.d.ts +5 -0
- package/build/manager/utils/trusted.d.ts.map +1 -0
- package/build/manager/utils/trusted.js +11 -0
- package/build/manager/utils/trusted.js.map +1 -0
- package/build/model/index.d.ts +2 -0
- package/build/model/index.d.ts.map +1 -0
- package/build/model/index.js +2 -0
- package/build/model/index.js.map +1 -0
- package/build/model/rely/consumer.d.ts +3 -0
- package/build/model/rely/consumer.d.ts.map +1 -0
- package/build/model/rely/consumer.js +9 -0
- package/build/model/rely/consumer.js.map +1 -0
- package/build/model/rely/index.d.ts +5 -0
- package/build/model/rely/index.d.ts.map +1 -0
- package/build/model/rely/index.js +4 -0
- package/build/model/rely/index.js.map +1 -0
- package/build/model/rely/peer.d.ts +5 -0
- package/build/model/rely/peer.d.ts.map +1 -0
- package/build/model/rely/peer.js +36 -0
- package/build/model/rely/peer.js.map +1 -0
- package/build/model/rely/provider.d.ts +3 -0
- package/build/model/rely/provider.d.ts.map +1 -0
- package/build/model/rely/provider.js +6 -0
- package/build/model/rely/provider.js.map +1 -0
- package/build/model/rely/types.d.ts +8 -0
- package/build/model/rely/types.d.ts.map +1 -0
- package/build/model/rely/types.js +2 -0
- package/build/model/rely/types.js.map +1 -0
- package/build/modules.d.ts +3 -0
- package/build/modules.d.ts.map +1 -0
- package/build/modules.js +11 -0
- package/build/modules.js.map +1 -0
- package/build/service.d.ts +7 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +90 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +13 -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 +76 -0
- package/src/actions/index.ts +2 -0
- package/src/actions/service.ts +16 -0
- package/src/consts.ts +8 -0
- package/src/index.ts +6 -0
- package/src/manager/README.md +507 -0
- package/src/manager/actions/index.ts +2 -0
- package/src/manager/actions/manager.ts +23 -0
- package/src/manager/consts.ts +4 -0
- package/src/manager/context.ts +33 -0
- package/src/manager/exports.ts +17 -0
- package/src/manager/index.ts +11 -0
- package/src/manager/main.ts +9 -0
- package/src/manager/model.ts +93 -0
- package/src/manager/modules.ts +29 -0
- package/src/manager/plugins/basic-ed25519.ts +56 -0
- package/src/manager/plugins/basic-rely.ts +167 -0
- package/src/manager/plugins/export.ts +6 -0
- package/src/manager/plugins/index.ts +13 -0
- package/src/manager/plugins/re-captcha.ts +62 -0
- package/src/manager/plugins/types.ts +27 -0
- package/src/manager/plugins/utils.ts +21 -0
- package/src/manager/rely/flow.ts +135 -0
- package/src/manager/rely/index.ts +2 -0
- package/src/manager/rely.ts +90 -0
- package/src/manager/types.ts +42 -0
- package/src/manager/utils/index.ts +2 -0
- package/src/manager/utils/trusted.ts +16 -0
- package/src/model/index.ts +2 -0
- package/src/model/rely/consumer.ts +11 -0
- package/src/model/rely/index.ts +5 -0
- package/src/model/rely/peer.ts +43 -0
- package/src/model/rely/provider.ts +9 -0
- package/src/model/rely/types.ts +8 -0
- package/src/modules.ts +17 -0
- package/src/service.ts +126 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +15 -0
- 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,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
|
+
}
|