@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.
Files changed (61) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/constants/error-code.d.ts +1 -0
  3. package/dist-server/constants/error-code.js +2 -1
  4. package/dist-server/constants/error-code.js.map +1 -1
  5. package/dist-server/controllers/profile.d.ts +1 -0
  6. package/dist-server/middlewares/authenticate-401-middleware.js +1 -1
  7. package/dist-server/middlewares/authenticate-401-middleware.js.map +1 -1
  8. package/dist-server/middlewares/index.d.ts +1 -0
  9. package/dist-server/middlewares/index.js +2 -1
  10. package/dist-server/middlewares/index.js.map +1 -1
  11. package/dist-server/middlewares/webauthn-middleware.d.ts +2 -0
  12. package/dist-server/middlewares/webauthn-middleware.js +54 -0
  13. package/dist-server/middlewares/webauthn-middleware.js.map +1 -0
  14. package/dist-server/router/index.d.ts +1 -0
  15. package/dist-server/router/index.js +1 -0
  16. package/dist-server/router/index.js.map +1 -1
  17. package/dist-server/router/webauthn-router.d.ts +2 -0
  18. package/dist-server/router/webauthn-router.js +45 -0
  19. package/dist-server/router/webauthn-router.js.map +1 -0
  20. package/dist-server/routes.js +3 -1
  21. package/dist-server/routes.js.map +1 -1
  22. package/dist-server/service/auth-provider/auth-provider-type.js.map +1 -1
  23. package/dist-server/service/auth-provider/auth-provider.d.ts +0 -5
  24. package/dist-server/service/auth-provider/auth-provider.js +1 -15
  25. package/dist-server/service/auth-provider/auth-provider.js.map +1 -1
  26. package/dist-server/service/index.d.ts +2 -1
  27. package/dist-server/service/index.js +4 -1
  28. package/dist-server/service/index.js.map +1 -1
  29. package/dist-server/service/user/user.d.ts +2 -0
  30. package/dist-server/service/user/user.js +14 -24
  31. package/dist-server/service/user/user.js.map +1 -1
  32. package/dist-server/service/web-auth-credential/index.d.ts +2 -0
  33. package/dist-server/service/web-auth-credential/index.js +6 -0
  34. package/dist-server/service/web-auth-credential/index.js.map +1 -0
  35. package/dist-server/service/web-auth-credential/web-auth-credential.d.ts +15 -0
  36. package/dist-server/service/web-auth-credential/web-auth-credential.js +72 -0
  37. package/dist-server/service/web-auth-credential/web-auth-credential.js.map +1 -0
  38. package/dist-server/tsconfig.tsbuildinfo +1 -1
  39. package/dist-server/utils/access-token-cookie.d.ts +1 -0
  40. package/dist-server/utils/access-token-cookie.js +11 -1
  41. package/dist-server/utils/access-token-cookie.js.map +1 -1
  42. package/package.json +7 -4
  43. package/server/constants/error-code.ts +1 -0
  44. package/server/middlewares/authenticate-401-middleware.ts +1 -1
  45. package/server/middlewares/index.ts +2 -1
  46. package/server/middlewares/webauthn-middleware.ts +68 -0
  47. package/server/router/index.ts +1 -0
  48. package/server/router/webauthn-router.ts +56 -0
  49. package/server/routes.ts +7 -8
  50. package/server/service/auth-provider/auth-provider-type.ts +3 -7
  51. package/server/service/auth-provider/auth-provider.ts +2 -18
  52. package/server/service/index.ts +5 -5
  53. package/server/service/user/user.ts +12 -22
  54. package/server/service/web-auth-credential/index.ts +3 -0
  55. package/server/service/web-auth-credential/web-auth-credential.ts +66 -0
  56. package/server/utils/access-token-cookie.ts +11 -0
  57. package/translations/en.json +29 -27
  58. package/translations/ja.json +30 -28
  59. package/translations/ko.json +30 -28
  60. package/translations/ms.json +3 -1
  61. 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.81",
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.71",
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.71",
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": "a84eff7066765905bf6a19fb61c641814bb7cac6"
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: accessTokenCookieKey,
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
+ }
@@ -6,3 +6,4 @@ export * from './oauth2'
6
6
  export * from './auth-checkin-router'
7
7
  export * from './auth-signin-router'
8
8
  export * from './auth-signup-router'
9
+ export * from './webauthn-router'
@@ -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 type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
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 { ObjectRef, ScalarObject } from '@things-factory/shell'
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, registerEnumType } from 'type-graphql'
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 })
@@ -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,3 @@
1
+ import { WebAuthCredential } from './web-auth-credential'
2
+
3
+ export const entities = [WebAuthCredential]
@@ -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
 
@@ -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.pattern_minimum_charaters": "minimum {length} charaters",
22
- "text.pattern_atleast_1_lowercase": "at least 1 lowercase character",
23
- "text.pattern_atleast_1_uppercase": "at least 1 uppercase character",
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.password reset email sent": "password reset email sent",
35
- "text.verification email sent": "verification email sent",
36
- "error.confirm password not matched": "new password and confirm password is not matched",
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
  }