@things-factory/auth-base 7.0.1-alpha.81 → 7.0.1-alpha.90
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/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/constants/error-code.d.ts +1 -0
- package/dist-server/constants/error-code.js +2 -1
- package/dist-server/constants/error-code.js.map +1 -1
- package/dist-server/controllers/profile.d.ts +1 -0
- package/dist-server/middlewares/authenticate-401-middleware.js +1 -1
- package/dist-server/middlewares/authenticate-401-middleware.js.map +1 -1
- package/dist-server/middlewares/index.d.ts +1 -0
- package/dist-server/middlewares/index.js +2 -1
- package/dist-server/middlewares/index.js.map +1 -1
- package/dist-server/middlewares/webauthn-middleware.d.ts +2 -0
- package/dist-server/middlewares/webauthn-middleware.js +54 -0
- package/dist-server/middlewares/webauthn-middleware.js.map +1 -0
- package/dist-server/router/index.d.ts +1 -0
- package/dist-server/router/index.js +1 -0
- package/dist-server/router/index.js.map +1 -1
- package/dist-server/router/webauthn-router.d.ts +2 -0
- package/dist-server/router/webauthn-router.js +45 -0
- package/dist-server/router/webauthn-router.js.map +1 -0
- package/dist-server/routes.js +3 -1
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/auth-provider/auth-provider-type.js.map +1 -1
- package/dist-server/service/auth-provider/auth-provider.d.ts +0 -5
- package/dist-server/service/auth-provider/auth-provider.js +1 -15
- package/dist-server/service/auth-provider/auth-provider.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +4 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/user/user.d.ts +2 -0
- package/dist-server/service/user/user.js +14 -24
- package/dist-server/service/user/user.js.map +1 -1
- package/dist-server/service/web-auth-credential/index.d.ts +2 -0
- package/dist-server/service/web-auth-credential/index.js +6 -0
- package/dist-server/service/web-auth-credential/index.js.map +1 -0
- package/dist-server/service/web-auth-credential/web-auth-credential.d.ts +15 -0
- package/dist-server/service/web-auth-credential/web-auth-credential.js +72 -0
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/access-token-cookie.d.ts +1 -0
- package/dist-server/utils/access-token-cookie.js +11 -1
- package/dist-server/utils/access-token-cookie.js.map +1 -1
- package/package.json +7 -4
- package/server/constants/error-code.ts +1 -0
- package/server/middlewares/authenticate-401-middleware.ts +1 -1
- package/server/middlewares/index.ts +2 -1
- package/server/middlewares/webauthn-middleware.ts +68 -0
- package/server/router/index.ts +1 -0
- package/server/router/webauthn-router.ts +56 -0
- package/server/routes.ts +7 -8
- package/server/service/auth-provider/auth-provider-type.ts +3 -7
- package/server/service/auth-provider/auth-provider.ts +2 -18
- package/server/service/index.ts +5 -5
- package/server/service/user/user.ts +12 -22
- package/server/service/web-auth-credential/index.ts +3 -0
- package/server/service/web-auth-credential/web-auth-credential.ts +66 -0
- package/server/utils/access-token-cookie.ts +11 -0
- package/translations/en.json +29 -27
- package/translations/ja.json +30 -28
- package/translations/ko.json +30 -28
- package/translations/ms.json +3 -1
- package/translations/zh.json +3 -1
@@ -1,3 +1,4 @@
|
|
1
1
|
export declare function getAccessTokenCookie(context: any): any;
|
2
2
|
export declare function setAccessTokenCookie(context: any, token: any): void;
|
3
|
+
export declare function setSessionAccessToken(context: any): void;
|
3
4
|
export declare function clearAccessTokenCookie(context: any): void;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.clearAccessTokenCookie = exports.setAccessTokenCookie = exports.getAccessTokenCookie = void 0;
|
3
|
+
exports.clearAccessTokenCookie = exports.setSessionAccessToken = exports.setAccessTokenCookie = exports.getAccessTokenCookie = void 0;
|
4
4
|
const shell_1 = require("@things-factory/shell");
|
5
5
|
const env_1 = require("@things-factory/env");
|
6
6
|
const max_age_1 = require("../constants/max-age");
|
@@ -24,6 +24,16 @@ function setAccessTokenCookie(context, token) {
|
|
24
24
|
context.cookies.set(accessTokenCookieKey, token, cookie);
|
25
25
|
}
|
26
26
|
exports.setAccessTokenCookie = setAccessTokenCookie;
|
27
|
+
function setSessionAccessToken(context) {
|
28
|
+
/* koa-session 을 사용하는 경우에는, cookie 직접 설정이 작동되지 않는다. 그런 경우에는 session에 설정해서 cookie를 변경한다. */
|
29
|
+
const { user } = context.state;
|
30
|
+
context.session = {
|
31
|
+
"id": user.id,
|
32
|
+
"userType": user.type,
|
33
|
+
"status": user.state
|
34
|
+
};
|
35
|
+
}
|
36
|
+
exports.setSessionAccessToken = setSessionAccessToken;
|
27
37
|
function clearAccessTokenCookie(context) {
|
28
38
|
const { secure } = context;
|
29
39
|
var cookie = {
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"access-token-cookie.js","sourceRoot":"","sources":["../../server/utils/access-token-cookie.ts"],"names":[],"mappings":";;;AAAA,iDAAmE;AACnE,6CAA4C;AAC5C,kDAA8C;AAE9C,MAAM,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAA;AAE/E,SAAgB,oBAAoB,CAAC,OAAO;;IAC1C,OAAO,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,OAAO,0CAAE,GAAG,CAAC,oBAAoB,CAAC,CAAA;AACpD,CAAC;AAFD,oDAEC;AAED,SAAgB,oBAAoB,CAAC,OAAO,EAAE,KAAK;IACjD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,iBAAO;KAChB,CAAA;IAED,MAAM,YAAY,GAAG,IAAA,mCAA2B,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,IAAI,YAAY,EAAE;QAChB,MAAM,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAA;KAChC;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AAC1D,CAAC;AAfD,oDAeC;AAED,SAAgB,sBAAsB,CAAC,OAAO;IAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;KACf,CAAA;IAED,MAAM,YAAY,GAAG,IAAA,mCAA2B,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,IAAI,YAAY,EAAE;QAChB,MAAM,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAA;KAChC;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IACrD;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;AAC5C,CAAC;AAnBD,wDAmBC","sourcesContent":["import { getCookieDomainFromHostname } from '@things-factory/shell'\nimport { config } from '@things-factory/env'\nimport { MAX_AGE } from '../constants/max-age'\n\nconst accessTokenCookieKey = config.get('accessTokenCookieKey', 'access_token')\n\nexport function getAccessTokenCookie(context) {\n return context?.cookies?.get(accessTokenCookieKey)\n}\n\nexport function setAccessTokenCookie(context, token) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n maxAge: MAX_AGE\n }\n\n const cookieDomain = getCookieDomainFromHostname(context.hostname)\n if (cookieDomain) {\n cookie['domain'] = cookieDomain\n }\n\n context.cookies.set(accessTokenCookieKey, token, cookie)\n}\n\nexport function clearAccessTokenCookie(context) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true\n }\n\n const cookieDomain = getCookieDomainFromHostname(context.hostname)\n if (cookieDomain) {\n cookie['domain'] = cookieDomain\n }\n\n context.cookies.set(accessTokenCookieKey, '', cookie)\n /*\n * TODO clear i18next cookie as well - need to support domain\n * https://github.com/hatiolab/things-factory/issues/70\n */\n context.cookies.set('i18next', '', cookie)\n}\n"]}
|
1
|
+
{"version":3,"file":"access-token-cookie.js","sourceRoot":"","sources":["../../server/utils/access-token-cookie.ts"],"names":[],"mappings":";;;AAAA,iDAAmE;AACnE,6CAA4C;AAC5C,kDAA8C;AAE9C,MAAM,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAA;AAE/E,SAAgB,oBAAoB,CAAC,OAAO;;IAC1C,OAAO,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,OAAO,0CAAE,GAAG,CAAC,oBAAoB,CAAC,CAAA;AACpD,CAAC;AAFD,oDAEC;AAED,SAAgB,oBAAoB,CAAC,OAAO,EAAE,KAAK;IACjD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,iBAAO;KAChB,CAAA;IAED,MAAM,YAAY,GAAG,IAAA,mCAA2B,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,IAAI,YAAY,EAAE;QAChB,MAAM,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAA;KAChC;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AAC1D,CAAC;AAfD,oDAeC;AAED,SAAgB,qBAAqB,CAAC,OAAO;IAC3C,0FAA0F;IAC1F,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE9B,OAAO,CAAC,OAAO,GAAG;QAChB,IAAI,EAAE,IAAI,CAAC,EAAE;QACb,UAAU,EAAE,IAAI,CAAC,IAAI;QACrB,QAAQ,EAAE,IAAI,CAAC,KAAK;KACrB,CAAA;AACH,CAAC;AATD,sDASC;AAED,SAAgB,sBAAsB,CAAC,OAAO;IAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;KACf,CAAA;IAED,MAAM,YAAY,GAAG,IAAA,mCAA2B,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,IAAI,YAAY,EAAE;QAChB,MAAM,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAA;KAChC;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IACrD;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;AAC5C,CAAC;AAnBD,wDAmBC","sourcesContent":["import { getCookieDomainFromHostname } from '@things-factory/shell'\nimport { config } from '@things-factory/env'\nimport { MAX_AGE } from '../constants/max-age'\n\nconst accessTokenCookieKey = config.get('accessTokenCookieKey', 'access_token')\n\nexport function getAccessTokenCookie(context) {\n return context?.cookies?.get(accessTokenCookieKey)\n}\n\nexport function setAccessTokenCookie(context, token) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n maxAge: MAX_AGE\n }\n\n const cookieDomain = getCookieDomainFromHostname(context.hostname)\n if (cookieDomain) {\n cookie['domain'] = cookieDomain\n }\n\n context.cookies.set(accessTokenCookieKey, token, cookie)\n}\n\nexport function setSessionAccessToken(context) {\n /* koa-session 을 사용하는 경우에는, cookie 직접 설정이 작동되지 않는다. 그런 경우에는 session에 설정해서 cookie를 변경한다. */\n const { user } = context.state\n\n context.session = {\n \"id\": user.id,\n \"userType\": user.type,\n \"status\": user.state\n }\n}\n\nexport function clearAccessTokenCookie(context) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true\n }\n\n const cookieDomain = getCookieDomainFromHostname(context.hostname)\n if (cookieDomain) {\n cookie['domain'] = cookieDomain\n }\n\n context.cookies.set(accessTokenCookieKey, '', cookie)\n /*\n * TODO clear i18next cookie as well - need to support domain\n * https://github.com/hatiolab/things-factory/issues/70\n */\n context.cookies.set('i18next', '', cookie)\n}\n"]}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@things-factory/auth-base",
|
3
|
-
"version": "7.0.1-alpha.
|
3
|
+
"version": "7.0.1-alpha.90",
|
4
4
|
"main": "dist-server/index.js",
|
5
5
|
"browser": "dist-client/index.js",
|
6
6
|
"things-factory": true,
|
@@ -30,17 +30,20 @@
|
|
30
30
|
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create -d ./server/migrations"
|
31
31
|
},
|
32
32
|
"dependencies": {
|
33
|
-
"@things-factory/email-base": "^7.0.1-alpha.
|
33
|
+
"@things-factory/email-base": "^7.0.1-alpha.90",
|
34
34
|
"@things-factory/env": "^7.0.1-alpha.71",
|
35
|
-
"@things-factory/shell": "^7.0.1-alpha.
|
35
|
+
"@things-factory/shell": "^7.0.1-alpha.90",
|
36
36
|
"@things-factory/utils": "^7.0.1-alpha.71",
|
37
|
+
"@types/webappsec-credential-management": "^0.6.8",
|
37
38
|
"jsonwebtoken": "^9.0.0",
|
38
39
|
"koa-passport": "^6.0.0",
|
39
40
|
"koa-session": "^6.4.0",
|
40
41
|
"oauth2orize-koa": "^1.3.2",
|
42
|
+
"passport": "^0.7.0",
|
43
|
+
"passport-fido2-webauthn": "^0.1.0",
|
41
44
|
"passport-jwt": "^4.0.0",
|
42
45
|
"passport-local": "^1.0.0",
|
43
46
|
"popsicle-cookie-jar": "^1.0.0"
|
44
47
|
},
|
45
|
-
"gitHead": "
|
48
|
+
"gitHead": "79086a82c81dadca6c105a37fa91a2cd34a74183"
|
46
49
|
}
|
@@ -15,3 +15,4 @@ export const PASSWORD_PATTERN_NOT_MATCHED = 'password should match the rule'
|
|
15
15
|
export const USER_DUPLICATED = 'user duplicated'
|
16
16
|
export const PASSWORD_USED_PAST = 'password used in the past'
|
17
17
|
export const VERIFICATION_ERROR = 'user or verification token not found'
|
18
|
+
export const USER_CREDENTIAL_NOT_FOUND = 'user credential not found'
|
@@ -15,7 +15,7 @@ export async function authenticate401Middleware(context, next) {
|
|
15
15
|
var message
|
16
16
|
|
17
17
|
if (err instanceof AuthError) {
|
18
|
-
message = context.t(`error.${err.errorCode}`, err.detail || {})
|
18
|
+
message = (context.t && context.t(`error.${err.errorCode}`, err.detail || {})) || err.errorCode
|
19
19
|
} else {
|
20
20
|
if (err?.status !== 401) {
|
21
21
|
throw err
|
@@ -20,7 +20,7 @@ export function initMiddlewares(app: any) {
|
|
20
20
|
app.use(
|
21
21
|
session(
|
22
22
|
{
|
23
|
-
key:
|
23
|
+
key: 'tfsession',
|
24
24
|
maxAge: MAX_AGE,
|
25
25
|
overwrite: true,
|
26
26
|
httpOnly: true,
|
@@ -63,4 +63,5 @@ process.on('bootstrap-module-subscription' as any, (app, subscriptionMiddleware)
|
|
63
63
|
export * from './jwt-authenticate-middleware'
|
64
64
|
export * from './domain-authenticate-middleware'
|
65
65
|
export * from './signin-middleware'
|
66
|
+
export * from './webauthn-middleware'
|
66
67
|
export * from './authenticate-401-middleware'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import passport from 'koa-passport'
|
2
|
+
import { Strategy as WebAuthnStrategy, SessionChallengeStore } from 'passport-fido2-webauthn'
|
3
|
+
|
4
|
+
import { getRepository } from '@things-factory/shell'
|
5
|
+
|
6
|
+
import { User } from '../service/user/user'
|
7
|
+
import { AuthError } from '../errors/auth-error'
|
8
|
+
import { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential'
|
9
|
+
|
10
|
+
export const store = new SessionChallengeStore()
|
11
|
+
|
12
|
+
passport.use(
|
13
|
+
new WebAuthnStrategy(
|
14
|
+
{ store },
|
15
|
+
async function verify(id: string, userHandle: Uint8Array, cb) {
|
16
|
+
const user = await getRepository(User).findOne({ where: { email: userHandle.toString() } })
|
17
|
+
if (!user) {
|
18
|
+
return cb(null, false, { errorCode: AuthError.ERROR_CODES.USER_NOT_FOUND })
|
19
|
+
}
|
20
|
+
const credential = await getRepository(WebAuthCredential).findOne({
|
21
|
+
where: { credentialId: id, user: { id: user.id } }
|
22
|
+
})
|
23
|
+
if (!credential) {
|
24
|
+
return cb(null, false, { errorCode: AuthError.ERROR_CODES.USER_CREDENTIAL_NOT_FOUND })
|
25
|
+
}
|
26
|
+
|
27
|
+
return cb(null, user, credential.publicKey)
|
28
|
+
},
|
29
|
+
async function register(user, id, publicKey, cb) {
|
30
|
+
const userObject = await getRepository(User).findOne({ where: { email: user.id.toString() } })
|
31
|
+
const webAuthRepository = getRepository(WebAuthCredential)
|
32
|
+
|
33
|
+
const oldCredential = await webAuthRepository.findOne({
|
34
|
+
where: { user: { id: userObject.id }, publicKey: publicKey }
|
35
|
+
})
|
36
|
+
|
37
|
+
/* TODO publicKey 비교로는 중복된 등록을 막을 수 없다. */
|
38
|
+
if (oldCredential) {
|
39
|
+
await webAuthRepository.delete(oldCredential.id)
|
40
|
+
}
|
41
|
+
|
42
|
+
await webAuthRepository.save({
|
43
|
+
user: userObject,
|
44
|
+
credentialId: id,
|
45
|
+
publicKey,
|
46
|
+
counter: 0
|
47
|
+
})
|
48
|
+
|
49
|
+
return cb(null, userObject)
|
50
|
+
}
|
51
|
+
)
|
52
|
+
)
|
53
|
+
|
54
|
+
export async function webAuthnMiddleware(context, next) {
|
55
|
+
return passport.authenticate(
|
56
|
+
'webauthn',
|
57
|
+
{ session: true, failureMessage: true, failWithError: true },
|
58
|
+
async (err, user, info) => {
|
59
|
+
if (err || !user) {
|
60
|
+
throw new AuthError(info)
|
61
|
+
} else {
|
62
|
+
context.state.user = user
|
63
|
+
|
64
|
+
await next()
|
65
|
+
}
|
66
|
+
}
|
67
|
+
)(context, next)
|
68
|
+
}
|
package/server/router/index.ts
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
import util from 'util'
|
2
|
+
import Router from 'koa-router'
|
3
|
+
|
4
|
+
import { accepts } from '../utils/accepts'
|
5
|
+
import { setAccessTokenCookie } from '../utils/access-token-cookie'
|
6
|
+
import { store, webAuthnMiddleware } from '../middlewares/webauthn-middleware'
|
7
|
+
|
8
|
+
export const webAuthnGlobalPublicRouter = new Router()
|
9
|
+
export const webAuthnGlobalPrivateRouter = new Router()
|
10
|
+
|
11
|
+
const challengeAsync = util.promisify(store.challenge).bind(store)
|
12
|
+
|
13
|
+
webAuthnGlobalPublicRouter.post('/auth/signin-webauthn/challenge', async (context, next) => {
|
14
|
+
const challenge = await challengeAsync({ ...context.request, session: context.session })
|
15
|
+
|
16
|
+
context.body = {
|
17
|
+
challenge: Buffer.from(challenge).toString('base64')
|
18
|
+
}
|
19
|
+
})
|
20
|
+
|
21
|
+
webAuthnGlobalPublicRouter.post('/auth/signin-webauthn', webAuthnMiddleware, async (context, next) => {
|
22
|
+
const { domain, user } = context.state
|
23
|
+
const { request } = context
|
24
|
+
const { body: reqBody } = request
|
25
|
+
|
26
|
+
const token = await user.sign({ subdomain: domain?.subdomain })
|
27
|
+
setAccessTokenCookie(context, token)
|
28
|
+
|
29
|
+
var redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(reqBody.redirectTo || '/')}`
|
30
|
+
|
31
|
+
/* 2단계 인터렉션 때문에 브라우저에서 fetch(...)로 진행될 것이므로, redirect(3xx) 응답으로 처리할 수 없다. 따라서, 데이타로 redirectURL를 응답한다. */
|
32
|
+
context.body = { redirectURL }
|
33
|
+
})
|
34
|
+
|
35
|
+
webAuthnGlobalPrivateRouter.post('/auth/register-webauthn/challenge', async (context, next) => {
|
36
|
+
const { user } = context.state
|
37
|
+
const { email, name } = user || {}
|
38
|
+
|
39
|
+
const challenge = await challengeAsync(
|
40
|
+
{ ...context.request, session: context.session },
|
41
|
+
{
|
42
|
+
user: {
|
43
|
+
id: email
|
44
|
+
}
|
45
|
+
}
|
46
|
+
)
|
47
|
+
|
48
|
+
context.body = {
|
49
|
+
user: {
|
50
|
+
id: Buffer.from(email).toString('base64'),
|
51
|
+
name: name,
|
52
|
+
displayName: name
|
53
|
+
},
|
54
|
+
challenge: Buffer.from(challenge).toString('base64')
|
55
|
+
}
|
56
|
+
})
|
package/server/routes.ts
CHANGED
@@ -10,11 +10,12 @@ import {
|
|
10
10
|
oauth2AuthorizeRouter,
|
11
11
|
oauth2Router,
|
12
12
|
pathBaseDomainRouter,
|
13
|
-
siteRootRouter
|
13
|
+
siteRootRouter,
|
14
|
+
webAuthnGlobalPublicRouter,
|
15
|
+
webAuthnGlobalPrivateRouter
|
14
16
|
} from './router'
|
15
17
|
|
16
18
|
import { setAccessTokenCookie } from './utils/access-token-cookie'
|
17
|
-
import { User } from './service/user/user'
|
18
19
|
|
19
20
|
const isPathBaseDomain = !config.get('subdomain') && !config.get('useVirtualHostBasedDomain')
|
20
21
|
|
@@ -27,7 +28,7 @@ process.on('bootstrap-module-global-public-route' as any, (app, globalPublicRout
|
|
27
28
|
authSigninRouter.get('/auth/sso-signin', app.ssoMiddlewares[0], async context => {
|
28
29
|
const { user } = context.state
|
29
30
|
|
30
|
-
const token = user.sign()
|
31
|
+
const token = await user.sign()
|
31
32
|
setAccessTokenCookie(context, token)
|
32
33
|
|
33
34
|
context.redirect('/auth/checkin')
|
@@ -41,12 +42,14 @@ process.on('bootstrap-module-global-private-route' as any, (app, globalPrivateRo
|
|
41
42
|
/* globalPrivateRouter based nested-routers */
|
42
43
|
globalPrivateRouter.use(authCheckinRouter.routes(), authCheckinRouter.allowedMethods())
|
43
44
|
globalPrivateRouter.use(authPrivateProcessRouter.routes(), authPrivateProcessRouter.allowedMethods())
|
45
|
+
globalPrivateRouter.use(webAuthnGlobalPrivateRouter.routes(), webAuthnGlobalPrivateRouter.allowedMethods())
|
44
46
|
})
|
45
47
|
|
46
48
|
process.on('bootstrap-module-domain-public-route' as any, (app, domainPublicRouter) => {
|
47
49
|
/* domainPublicRouter based nested-routers */
|
48
50
|
domainPublicRouter.use(authSigninRouter.routes(), authSigninRouter.allowedMethods())
|
49
51
|
domainPublicRouter.use(authSignupRouter.routes(), authSignupRouter.allowedMethods())
|
52
|
+
domainPublicRouter.use(webAuthnGlobalPublicRouter.routes(), webAuthnGlobalPublicRouter.allowedMethods())
|
50
53
|
|
51
54
|
/* path '/admin/oauth/...' is deprecated. should use path '/oauth/...' for oauth2 related routing */
|
52
55
|
domainPublicRouter.use('/oauth', oauth2Router.routes(), oauth2Router.allowedMethods()) // if i use context
|
@@ -61,11 +64,7 @@ process.on('bootstrap-module-domain-private-route' as any, (app, domainPrivateRo
|
|
61
64
|
// pathBaseDomainRouter는 history-fallback의 경우에 인증 처리를 하기 위한 라우터이다.
|
62
65
|
// (보통, URL 링크등을 통해서 domain path URL로 바로 요청하는 경우에 해당한다.)
|
63
66
|
// pathBaseDomainRouter는 domain path를 domain-private-router를 사용하는 것을 전제로 한다.
|
64
|
-
domainPrivateRouter.use(
|
65
|
-
'/domain/:domain/oauth',
|
66
|
-
oauth2AuthorizeRouter.routes(),
|
67
|
-
oauth2AuthorizeRouter.allowedMethods()
|
68
|
-
)
|
67
|
+
domainPrivateRouter.use('/domain/:domain/oauth', oauth2AuthorizeRouter.routes(), oauth2AuthorizeRouter.allowedMethods())
|
69
68
|
domainPrivateRouter.use('/domain', pathBaseDomainRouter.routes(), pathBaseDomainRouter.allowedMethods())
|
70
69
|
}
|
71
70
|
|
@@ -1,11 +1,7 @@
|
|
1
|
-
import
|
2
|
-
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
3
|
-
import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-graphql'
|
1
|
+
import { ObjectType, Field, InputType, Int, ID } from 'type-graphql'
|
4
2
|
|
5
|
-
import {
|
6
|
-
|
7
|
-
import { AuthProvider, AuthProviderStatus } from './auth-provider'
|
8
|
-
import { AuthProviderParameterSpec } from './auth-provider-parameter-spec'
|
3
|
+
import { ScalarObject } from '@things-factory/shell'
|
4
|
+
import { AuthProvider } from './auth-provider'
|
9
5
|
|
10
6
|
@InputType()
|
11
7
|
export class NewAuthProvider {
|
@@ -1,17 +1,15 @@
|
|
1
1
|
import {
|
2
2
|
CreateDateColumn,
|
3
3
|
UpdateDateColumn,
|
4
|
-
DeleteDateColumn,
|
5
4
|
Entity,
|
6
5
|
Index,
|
7
6
|
Column,
|
8
7
|
RelationId,
|
9
8
|
ManyToOne,
|
10
9
|
OneToMany,
|
11
|
-
PrimaryGeneratedColumn
|
12
|
-
VersionColumn
|
10
|
+
PrimaryGeneratedColumn
|
13
11
|
} from 'typeorm'
|
14
|
-
import { Directive, ObjectType, Field, Int, ID
|
12
|
+
import { Directive, ObjectType, Field, Int, ID } from 'type-graphql'
|
15
13
|
|
16
14
|
import { Domain, ScalarObject, encryptTransformer } from '@things-factory/shell'
|
17
15
|
import { User } from '../user/user'
|
@@ -30,16 +28,6 @@ export type AuthProviderRegistry = {
|
|
30
28
|
[type: string]: AuthProviderImpl
|
31
29
|
}
|
32
30
|
|
33
|
-
export enum AuthProviderStatus {
|
34
|
-
STATUS_A = 'STATUS_A',
|
35
|
-
STATUS_B = 'STATUS_B'
|
36
|
-
}
|
37
|
-
|
38
|
-
registerEnumType(AuthProviderStatus, {
|
39
|
-
name: 'AuthProviderStatus',
|
40
|
-
description: 'state enumeration of a authProvider'
|
41
|
-
})
|
42
|
-
|
43
31
|
@ObjectType()
|
44
32
|
export class AuthProviderType {
|
45
33
|
@Field()
|
@@ -89,10 +77,6 @@ export class AuthProvider {
|
|
89
77
|
@Field({ nullable: true })
|
90
78
|
active?: boolean
|
91
79
|
|
92
|
-
@Column({ nullable: true })
|
93
|
-
@Field({ nullable: true })
|
94
|
-
state?: AuthProviderStatus
|
95
|
-
|
96
80
|
@Directive('@privilege(category: "security", privilege: "query", domainOwnerGranted: true)')
|
97
81
|
@Column({ nullable: true })
|
98
82
|
@Field({ nullable: true })
|
package/server/service/index.ts
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
/* IMPORT ENTITIES AND RESOLVERS */
|
2
|
-
import {
|
3
|
-
entities as UsersAuthProvidersEntities,
|
4
|
-
resolvers as UsersAuthProvidersResolvers
|
5
|
-
} from './users-auth-providers'
|
2
|
+
import { entities as UsersAuthProvidersEntities, resolvers as UsersAuthProvidersResolvers } from './users-auth-providers'
|
6
3
|
import { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider'
|
7
4
|
import { resolvers as AppbindingResolver } from './app-binding'
|
8
5
|
import { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance'
|
@@ -18,6 +15,7 @@ import { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privil
|
|
18
15
|
import { entities as RoleEntities, resolvers as RoleResolvers } from './role'
|
19
16
|
import { entities as UserEntities, resolvers as UserResolvers } from './user'
|
20
17
|
import { entities as VerificationTokenEntities } from './verification-token'
|
18
|
+
import { entities as WebAuthCredentialEntities } from './web-auth-credential'
|
21
19
|
|
22
20
|
/* EXPORT ENTITY TYPES */
|
23
21
|
export * from './users-auth-providers/users-auth-providers'
|
@@ -34,6 +32,7 @@ export * from './app-binding/app-binding'
|
|
34
32
|
export * from './password-history/password-history'
|
35
33
|
export * from './verification-token/verification-token'
|
36
34
|
export * from './login-history/login-history'
|
35
|
+
export * from './web-auth-credential/web-auth-credential'
|
37
36
|
|
38
37
|
/* EXPORT TYPES */
|
39
38
|
export * from './app-binding/app-binding-types'
|
@@ -60,7 +59,8 @@ export const entities = [
|
|
60
59
|
...InvitationEntities,
|
61
60
|
...PasswordHistoryEntities,
|
62
61
|
...VerificationTokenEntities,
|
63
|
-
...LoginHistoryEntities
|
62
|
+
...LoginHistoryEntities,
|
63
|
+
...WebAuthCredentialEntities
|
64
64
|
]
|
65
65
|
|
66
66
|
export const schema = {
|
@@ -12,6 +12,7 @@ import { AuthError } from '../../errors/auth-error'
|
|
12
12
|
import { SECRET } from '../../utils/get-secret'
|
13
13
|
import { Role } from '../role/role'
|
14
14
|
import { Privilege } from '../privilege/privilege'
|
15
|
+
import { WebAuthCredential } from '../web-auth-credential/web-auth-credential'
|
15
16
|
import { UsersAuthProviders } from '../users-auth-providers/users-auth-providers'
|
16
17
|
import { getDomainsWithPrivilege } from '../../utils/get-user-domains'
|
17
18
|
|
@@ -31,7 +32,6 @@ export enum UserStatus {
|
|
31
32
|
|
32
33
|
@Entity()
|
33
34
|
@Index('ix_user_0', (user: User) => [user.email], { unique: true })
|
34
|
-
//@Index('ix_user_1', (user: User) => [user.id], { unique: true })
|
35
35
|
@ObjectType()
|
36
36
|
export class User {
|
37
37
|
@PrimaryGeneratedColumn('uuid')
|
@@ -39,12 +39,10 @@ export class User {
|
|
39
39
|
readonly id: string
|
40
40
|
|
41
41
|
@Column()
|
42
|
-
@Field()
|
42
|
+
@Field({ nullable: true })
|
43
43
|
name: string
|
44
44
|
|
45
|
-
@Column({
|
46
|
-
nullable: true
|
47
|
-
})
|
45
|
+
@Column({ nullable: true })
|
48
46
|
@Field({ nullable: true })
|
49
47
|
description: string
|
50
48
|
|
@@ -69,22 +67,16 @@ export class User {
|
|
69
67
|
@Field(type => [Role])
|
70
68
|
roles?: Role[]
|
71
69
|
|
72
|
-
@Column({
|
73
|
-
nullable: true
|
74
|
-
})
|
70
|
+
@Column({ nullable: true })
|
75
71
|
@Field({ nullable: true })
|
76
72
|
userType: string // default: 'user', enum: 'user', 'application', 'appliance'
|
77
73
|
|
78
|
-
@Column({
|
79
|
-
nullable: true
|
80
|
-
})
|
74
|
+
@Column({ nullable: true })
|
81
75
|
@Field({ nullable: true })
|
82
76
|
reference: string
|
83
77
|
|
84
78
|
@Directive('@privilege(category: "security", privilege: "query", domainOwnerGranted: true)')
|
85
|
-
@Column({
|
86
|
-
nullable: true
|
87
|
-
})
|
79
|
+
@Column({ nullable: true })
|
88
80
|
salt: string
|
89
81
|
|
90
82
|
@Column({ nullable: true })
|
@@ -104,20 +96,18 @@ export class User {
|
|
104
96
|
@Field(type => String)
|
105
97
|
status: UserStatus
|
106
98
|
|
107
|
-
@Column({
|
108
|
-
type: 'smallint',
|
109
|
-
default: 0
|
110
|
-
})
|
99
|
+
@Column({ type: 'smallint', default: 0 })
|
111
100
|
failCount: number
|
112
101
|
|
113
|
-
@Column({
|
114
|
-
nullable: true
|
115
|
-
})
|
102
|
+
@Column({ nullable: true })
|
116
103
|
passwordUpdatedAt: Date
|
117
104
|
|
118
105
|
@Field({ nullable: true })
|
119
106
|
owner: boolean /* should not be a column */
|
120
107
|
|
108
|
+
@OneToMany(() => WebAuthCredential, credential => credential.user)
|
109
|
+
credentials: WebAuthCredential[]
|
110
|
+
|
121
111
|
@OneToMany(() => UsersAuthProviders, usersAuthProviders => usersAuthProviders.user)
|
122
112
|
@Field(type => [UsersAuthProviders], { nullable: true })
|
123
113
|
usersAuthProviders: UsersAuthProviders[]
|
@@ -252,7 +242,7 @@ export class User {
|
|
252
242
|
const repository = getRepository(User)
|
253
243
|
var user = await repository.findOne({
|
254
244
|
where: { id: decoded.id },
|
255
|
-
relations: ['domains'],
|
245
|
+
relations: ['domains', 'credentials'],
|
256
246
|
cache: true
|
257
247
|
})
|
258
248
|
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import { Field, ID } from 'type-graphql'
|
2
|
+
import {
|
3
|
+
CreateDateColumn,
|
4
|
+
UpdateDateColumn,
|
5
|
+
Entity,
|
6
|
+
Index,
|
7
|
+
Column,
|
8
|
+
RelationId,
|
9
|
+
ManyToOne,
|
10
|
+
PrimaryGeneratedColumn
|
11
|
+
} from 'typeorm'
|
12
|
+
|
13
|
+
import { User } from '../user/user'
|
14
|
+
|
15
|
+
@Entity()
|
16
|
+
@Index(
|
17
|
+
'ix_web_auth_credential_0',
|
18
|
+
(webAuthCredential: WebAuthCredential) => [webAuthCredential.user, webAuthCredential.credentialId],
|
19
|
+
{ unique: true }
|
20
|
+
)
|
21
|
+
export class WebAuthCredential {
|
22
|
+
@PrimaryGeneratedColumn('uuid')
|
23
|
+
@Field(type => ID)
|
24
|
+
readonly id: string
|
25
|
+
|
26
|
+
@ManyToOne(type => User, { nullable: true })
|
27
|
+
@Field(type => User, { nullable: true })
|
28
|
+
user?: User
|
29
|
+
|
30
|
+
@RelationId((webAuthCredential: WebAuthCredential) => webAuthCredential.user)
|
31
|
+
userId?: string
|
32
|
+
|
33
|
+
@Column()
|
34
|
+
@Field({ nullable: true })
|
35
|
+
credentialId: string
|
36
|
+
|
37
|
+
@Column()
|
38
|
+
@Field({ nullable: true })
|
39
|
+
publicKey: string
|
40
|
+
|
41
|
+
@Column()
|
42
|
+
@Field({ nullable: true })
|
43
|
+
counter: number
|
44
|
+
|
45
|
+
@CreateDateColumn()
|
46
|
+
@Field({ nullable: true })
|
47
|
+
createdAt?: Date
|
48
|
+
|
49
|
+
@UpdateDateColumn()
|
50
|
+
@Field({ nullable: true })
|
51
|
+
updatedAt?: Date
|
52
|
+
|
53
|
+
@ManyToOne(type => User, { nullable: true })
|
54
|
+
@Field(type => User, { nullable: true })
|
55
|
+
creator?: User
|
56
|
+
|
57
|
+
@RelationId((webAuthCredential: WebAuthCredential) => webAuthCredential.creator)
|
58
|
+
creatorId?: string
|
59
|
+
|
60
|
+
@ManyToOne(type => User, { nullable: true })
|
61
|
+
@Field(type => User, { nullable: true })
|
62
|
+
updater?: User
|
63
|
+
|
64
|
+
@RelationId((webAuthCredential: WebAuthCredential) => webAuthCredential.updater)
|
65
|
+
updaterId?: string
|
66
|
+
}
|
@@ -25,6 +25,17 @@ export function setAccessTokenCookie(context, token) {
|
|
25
25
|
context.cookies.set(accessTokenCookieKey, token, cookie)
|
26
26
|
}
|
27
27
|
|
28
|
+
export function setSessionAccessToken(context) {
|
29
|
+
/* koa-session 을 사용하는 경우에는, cookie 직접 설정이 작동되지 않는다. 그런 경우에는 session에 설정해서 cookie를 변경한다. */
|
30
|
+
const { user } = context.state
|
31
|
+
|
32
|
+
context.session = {
|
33
|
+
"id": user.id,
|
34
|
+
"userType": user.type,
|
35
|
+
"status": user.state
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
28
39
|
export function clearAccessTokenCookie(context) {
|
29
40
|
const { secure } = context
|
30
41
|
|
package/translations/en.json
CHANGED
@@ -1,4 +1,20 @@
|
|
1
1
|
{
|
2
|
+
"error.confirm password not matched": "new password and confirm password is not matched",
|
3
|
+
"error.domain mismatch": "certificate is not for this domain",
|
4
|
+
"error.domain not allowed": "user not allowed domain `{subdomain}`",
|
5
|
+
"error.failed to find x": "failed to find {x}",
|
6
|
+
"error.password should match the rule": "password should match following rule. ${rule}",
|
7
|
+
"error.password used in the past": "password used in the past",
|
8
|
+
"error.subdomain not found": "domain not found",
|
9
|
+
"error.token or password is invalid": "token or password is invalid",
|
10
|
+
"error.unavailable-domain": "unavailable domain",
|
11
|
+
"error.user credential not found": "user credential not found. To use biometric authentication, you need to register your device first.",
|
12
|
+
"error.user duplicated": "user duplicated",
|
13
|
+
"error.user not activated": "user is not activated",
|
14
|
+
"error.user not found": "user not found",
|
15
|
+
"error.user or verification token not found": "user or verification token not found",
|
16
|
+
"error.user validation failed": "user validation failed",
|
17
|
+
"error.x is not a member of y": "{x} is not a member of {y}",
|
2
18
|
"field.active": "active",
|
3
19
|
"field.appliance_id": "appliance id",
|
4
20
|
"field.brand": "brand",
|
@@ -13,43 +29,29 @@
|
|
13
29
|
"field.user_account": "user account",
|
14
30
|
"field.user_type": "user type",
|
15
31
|
"label.partner": "partner",
|
32
|
+
"privilege.category.system": "system setting",
|
33
|
+
"privilege.description": "to {name} {category} data",
|
34
|
+
"privilege.name.mutation": "edit",
|
35
|
+
"privilege.name.query": "read",
|
16
36
|
"text.account is reactivated": "account is reactivated",
|
17
37
|
"text.delete account succeed": "delete account succeed",
|
18
38
|
"text.inactive user": "inactive user",
|
19
39
|
"text.invalid verification token": "invalid verification token",
|
20
40
|
"text.invitation email sent": "invitation email sent",
|
21
|
-
"text.
|
22
|
-
"text.
|
23
|
-
"text.
|
41
|
+
"text.password changed successfully": "password changed successfully",
|
42
|
+
"text.password reset email sent": "password reset email sent",
|
43
|
+
"text.password reset succeed": "password reset succeed",
|
24
44
|
"text.pattern_atleast_1_digit": "at least 1 digit character",
|
45
|
+
"text.pattern_atleast_1_lowercase": "at least 1 lowercase character",
|
25
46
|
"text.pattern_atleast_1_special": "at least 1 special character(!@#$%^&*())",
|
47
|
+
"text.pattern_atleast_1_uppercase": "at least 1 uppercase character",
|
48
|
+
"text.pattern_minimum_charaters": "minimum {length} charaters",
|
26
49
|
"text.pattern_not_allowed": "not allowed repeated charater",
|
27
|
-
"text.password reset succeed": "password reset succeed",
|
28
|
-
"text.password changed successfully": "password changed successfully",
|
29
50
|
"text.profile changed successfully": "profile changed successfully",
|
30
51
|
"text.result": "result",
|
31
52
|
"text.signout successfully": "signout successfully",
|
32
|
-
"text.user registered successfully": "user registered successfully. find your email to activate account",
|
33
53
|
"text.user activated successfully": "user activated successfully",
|
34
|
-
"text.
|
35
|
-
"text.
|
36
|
-
"
|
37
|
-
"error.domain not allowed": "user not allowed domain `{subdomain}`",
|
38
|
-
"error.domain mismatch": "certificate is not for this domain",
|
39
|
-
"error.failed to find x": "failed to find {x}",
|
40
|
-
"error.password should match the rule": "password should match following rule. ${rule}",
|
41
|
-
"error.password used in the past": "password used in the past",
|
42
|
-
"error.subdomain not found": "domain not found",
|
43
|
-
"error.token or password is invalid": "token or password is invalid",
|
44
|
-
"error.unavailable-domain": "unavailable domain",
|
45
|
-
"error.user not found": "user not found",
|
46
|
-
"error.user duplicated": "user duplicated",
|
47
|
-
"error.user or verification token not found": "user or verification token not found",
|
48
|
-
"error.user validation failed": "user validation failed",
|
49
|
-
"error.user not activated": "user is not activated",
|
50
|
-
"error.x is not a member of y": "{x} is not a member of {y}",
|
51
|
-
"privilege.category.system": "system setting",
|
52
|
-
"privilege.description": "to {name} {category} data",
|
53
|
-
"privilege.name.mutation": "edit",
|
54
|
-
"privilege.name.query": "read"
|
54
|
+
"text.user credential registered successfully": "device registration has been successfully completed. You can now use biometric authentication.",
|
55
|
+
"text.user registered successfully": "user registered successfully. find your email to activate account",
|
56
|
+
"text.verification email sent": "verification email sent"
|
55
57
|
}
|