@things-factory/auth-base 8.0.0-alpha.2 → 8.0.0-alpha.24

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 (36) hide show
  1. package/client/index.ts +1 -0
  2. package/client/verify-webauthn.ts +86 -0
  3. package/dist-client/index.d.ts +1 -0
  4. package/dist-client/index.js +1 -0
  5. package/dist-client/index.js.map +1 -1
  6. package/dist-client/tsconfig.tsbuildinfo +1 -1
  7. package/dist-client/verify-webauthn.d.ts +13 -0
  8. package/dist-client/verify-webauthn.js +72 -0
  9. package/dist-client/verify-webauthn.js.map +1 -0
  10. package/dist-server/index.d.ts +1 -0
  11. package/dist-server/index.js +1 -0
  12. package/dist-server/index.js.map +1 -1
  13. package/dist-server/middlewares/webauthn-middleware.js.map +1 -1
  14. package/dist-server/router/webauthn-router.js +51 -1
  15. package/dist-server/router/webauthn-router.js.map +1 -1
  16. package/dist-server/service/appliance/appliance.js +4 -1
  17. package/dist-server/service/appliance/appliance.js.map +1 -1
  18. package/dist-server/service/application/application.js +11 -3
  19. package/dist-server/service/application/application.js.map +1 -1
  20. package/dist-server/service/user/user.js +17 -3
  21. package/dist-server/service/user/user.js.map +1 -1
  22. package/dist-server/service/verification-token/verification-token.js +7 -2
  23. package/dist-server/service/verification-token/verification-token.js.map +1 -1
  24. package/dist-server/tsconfig.tsbuildinfo +1 -1
  25. package/dist-server/utils/check-user-has-role.d.ts +12 -0
  26. package/dist-server/utils/check-user-has-role.js +28 -0
  27. package/dist-server/utils/check-user-has-role.js.map +1 -0
  28. package/package.json +6 -6
  29. package/server/index.ts +1 -0
  30. package/server/middlewares/webauthn-middleware.ts +7 -8
  31. package/server/router/webauthn-router.ts +71 -9
  32. package/server/service/appliance/appliance.ts +4 -1
  33. package/server/service/application/application.ts +12 -3
  34. package/server/service/user/user.ts +33 -4
  35. package/server/service/verification-token/verification-token.ts +9 -3
  36. package/server/utils/check-user-has-role.ts +29 -0
@@ -0,0 +1,12 @@
1
+ import { Domain } from '@things-factory/shell';
2
+ import { User } from '../service/user/user';
3
+ /**
4
+ * @description 사용자가 특정 도메인 또는 상위 도메인에서 특정 역할을 가지고 있는지 확인합니다.
5
+ *
6
+ * @param roleId 확인할 역할의 ID
7
+ * @param domain 역할을 확인할 도메인
8
+ * @param user 역할을 확인할 사용자
9
+ *
10
+ * @returns 사용자가 도메인 또는 상위 도메인에서 역할을 가지고 있는지 여부를 나타내는 boolean을 반환하는 Promise
11
+ */
12
+ export declare function checkUserHasRole(roleId: string, domain: Domain, user: User): Promise<Boolean>;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkUserHasRole = checkUserHasRole;
4
+ const shell_1 = require("@things-factory/shell");
5
+ const user_1 = require("../service/user/user");
6
+ /**
7
+ * @description 사용자가 특정 도메인 또는 상위 도메인에서 특정 역할을 가지고 있는지 확인합니다.
8
+ *
9
+ * @param roleId 확인할 역할의 ID
10
+ * @param domain 역할을 확인할 도메인
11
+ * @param user 역할을 확인할 사용자
12
+ *
13
+ * @returns 사용자가 도메인 또는 상위 도메인에서 역할을 가지고 있는지 여부를 나타내는 boolean을 반환하는 Promise
14
+ */
15
+ async function checkUserHasRole(roleId, domain, user) {
16
+ if (!roleId) {
17
+ return true;
18
+ }
19
+ const me = await (0, shell_1.getRepository)(user_1.User).findOne({
20
+ where: { id: user.id },
21
+ relations: ['roles']
22
+ });
23
+ return me.roles
24
+ .filter(role => role.domainId === domain.id || (domain.parentId && role.domainId === domain.parentId))
25
+ .map(role => role.id)
26
+ .includes(roleId);
27
+ }
28
+ //# sourceMappingURL=check-user-has-role.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-user-has-role.js","sourceRoot":"","sources":["../../server/utils/check-user-has-role.ts"],"names":[],"mappings":";;AAcA,4CAcC;AA5BD,iDAA6D;AAE7D,+CAA2C;AAG3C;;;;;;;;GAQG;AACI,KAAK,UAAU,gBAAgB,CAAC,MAAc,EAAE,MAAc,EAAE,IAAU;IAC/E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,IAAA,qBAAa,EAAC,WAAI,CAAC,CAAC,OAAO,CAAC;QAC3C,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;QACtB,SAAS,EAAE,CAAC,OAAO,CAAC;KACrB,CAAC,CAAA;IAEF,OAAO,EAAE,CAAC,KAAK;SACZ,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;SACrG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;SACpB,QAAQ,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\n\nimport { User } from '../service/user/user'\nimport { Role } from 'service'\n\n/**\n * @description 사용자가 특정 도메인 또는 상위 도메인에서 특정 역할을 가지고 있는지 확인합니다.\n *\n * @param roleId 확인할 역할의 ID\n * @param domain 역할을 확인할 도메인\n * @param user 역할을 확인할 사용자\n *\n * @returns 사용자가 도메인 또는 상위 도메인에서 역할을 가지고 있는지 여부를 나타내는 boolean을 반환하는 Promise\n */\nexport async function checkUserHasRole(roleId: string, domain: Domain, user: User): Promise<Boolean> {\n if (!roleId) {\n return true\n }\n\n const me = await getRepository(User).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n\n return me.roles\n .filter(role => role.domainId === domain.id || (domain.parentId && role.domainId === domain.parentId))\n .map(role => role.id)\n .includes(roleId)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.24",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -32,10 +32,10 @@
32
32
  "dependencies": {
33
33
  "@simplewebauthn/browser": "^10.0.0",
34
34
  "@simplewebauthn/server": "^10.0.0",
35
- "@things-factory/email-base": "^8.0.0-alpha.2",
36
- "@things-factory/env": "^8.0.0-alpha.0",
37
- "@things-factory/shell": "^8.0.0-alpha.2",
38
- "@things-factory/utils": "^8.0.0-alpha.0",
35
+ "@things-factory/email-base": "^8.0.0-alpha.22",
36
+ "@things-factory/env": "^8.0.0-alpha.8",
37
+ "@things-factory/shell": "^8.0.0-alpha.22",
38
+ "@things-factory/utils": "^8.0.0-alpha.14",
39
39
  "@types/webappsec-credential-management": "^0.6.8",
40
40
  "jsonwebtoken": "^9.0.0",
41
41
  "koa-passport": "^6.0.0",
@@ -46,5 +46,5 @@
46
46
  "passport-jwt": "^4.0.0",
47
47
  "passport-local": "^1.0.0"
48
48
  },
49
- "gitHead": "9599f03becd1263f0c6d7c1138b82180036f7dbf"
49
+ "gitHead": "934692cc70c308351f4d3f720a26aed295d14486"
50
50
  }
package/server/index.ts CHANGED
@@ -17,6 +17,7 @@ export * from './utils/check-user-belongs-domain'
17
17
  export * from './utils/access-token-cookie'
18
18
  export * from './utils/encrypt-state'
19
19
  export * from './utils/check-permission'
20
+ export * from './utils/check-user-has-role'
20
21
 
21
22
  export * from './errors'
22
23
 
@@ -3,7 +3,6 @@ import { Strategy as CustomStrategy } from 'passport-custom'
3
3
 
4
4
  import { getRepository } from '@things-factory/shell'
5
5
 
6
- import { User } from '../service/user/user'
7
6
  import { AuthError } from '../errors/auth-error'
8
7
 
9
8
  import { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential'
@@ -57,23 +56,23 @@ passport.use(
57
56
  const { body, session, origin, hostname } = context as any
58
57
 
59
58
  const challenge = session.challenge
60
-
59
+
61
60
  const assertionResponse = body as {
62
61
  id: string
63
62
  response: AuthenticatorAssertionResponse
64
63
  }
65
-
64
+
66
65
  const credential = await getRepository(WebAuthCredential).findOne({
67
66
  where: {
68
67
  credentialId: assertionResponse.id
69
68
  },
70
69
  relations: ['user']
71
70
  })
72
-
71
+
73
72
  if (!credential) {
74
73
  return done(null, false)
75
74
  }
76
-
75
+
77
76
  const verification = await verifyAuthenticationResponse({
78
77
  response: body,
79
78
  expectedChallenge: challenge,
@@ -86,18 +85,18 @@ passport.use(
86
85
  counter: credential.counter
87
86
  }
88
87
  })
89
-
88
+
90
89
  if (verification.verified) {
91
90
  const { authenticationInfo } = verification
92
91
  credential.counter = authenticationInfo.newCounter
93
92
  await getRepository(WebAuthCredential).save(credential)
94
-
93
+
95
94
  const user = credential.user
96
95
  return done(null, user)
97
96
  } else {
98
97
  return done(verification, false)
99
98
  }
100
- } catch(error) {
99
+ } catch (error) {
101
100
  return done(error, false)
102
101
  }
103
102
  })
@@ -5,17 +5,75 @@ import { appPackage } from '@things-factory/env'
5
5
  import { generateRegistrationOptions, generateAuthenticationOptions } from '@simplewebauthn/server'
6
6
 
7
7
  import { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential'
8
- import {
9
- PublicKeyCredentialCreationOptionsJSON,
10
- } from '@simplewebauthn/server/script/deps'
8
+ import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/server/script/deps'
11
9
  import { setAccessTokenCookie } from '../utils/access-token-cookie'
12
- import { createWebAuthnMiddleware } from '../middlewares/webauthn-middleware';
10
+ import { createWebAuthnMiddleware } from '../middlewares/webauthn-middleware'
13
11
 
14
12
  export const webAuthnGlobalPublicRouter = new Router()
15
13
  export const webAuthnGlobalPrivateRouter = new Router()
16
14
 
17
15
  const { name: rpName } = appPackage as any
18
16
 
17
+ // Generate authentication challenge for the currently logged-in user
18
+ webAuthnGlobalPrivateRouter.get('/auth/verify-webauthn/challenge', async (context, next) => {
19
+ const { user } = context.state
20
+ const rpID = context.hostname
21
+
22
+ if (!user) {
23
+ context.status = 401
24
+ context.body = { error: 'User not authenticated' }
25
+ return
26
+ }
27
+
28
+ const webAuthCredentials = await getRepository(WebAuthCredential).find({
29
+ where: { user: { id: user.id } }
30
+ })
31
+
32
+ if (webAuthCredentials.length === 0) {
33
+ context.status = 400
34
+ context.body = { error: 'No biometric credentials registered for this user' }
35
+ return
36
+ }
37
+
38
+ const options = await generateAuthenticationOptions({
39
+ rpID,
40
+ userVerification: 'preferred',
41
+ allowCredentials: webAuthCredentials.map(credential => ({
42
+ id: credential.credentialId,
43
+ type: 'public-key'
44
+ }))
45
+ })
46
+
47
+ context.session.challenge = options.challenge
48
+ context.body = options
49
+ })
50
+
51
+ // Verify biometric authentication
52
+ webAuthnGlobalPrivateRouter.post(
53
+ '/auth/verify-webauthn',
54
+ /* reuse webauthn-login as webauthn-verify strategy */
55
+ createWebAuthnMiddleware('webauthn-login'),
56
+ async (context, next) => {
57
+ const { user } = context.state
58
+ const { request } = context
59
+ const { body: reqBody } = request
60
+
61
+ if (!user) {
62
+ context.status = 401
63
+ context.body = { verified: false, message: 'User not authenticated' }
64
+ return
65
+ }
66
+
67
+ context.body = {
68
+ verified: true,
69
+ message: 'Biometric authentication successful'
70
+ }
71
+
72
+ await next()
73
+ }
74
+ )
75
+
76
+ // Generate registration challenge for the currently logged-in user
19
77
  webAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async (context, next) => {
20
78
  const { user } = context.state
21
79
  const rpID = context.hostname
@@ -53,8 +111,10 @@ webAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async (cont
53
111
  context.body = options
54
112
  })
55
113
 
56
- webAuthnGlobalPrivateRouter.post('/auth/verify-registration', createWebAuthnMiddleware('webauthn-register'));
114
+ // Verify registration
115
+ webAuthnGlobalPrivateRouter.post('/auth/verify-registration', createWebAuthnMiddleware('webauthn-register'))
57
116
 
117
+ // Generate sign-in challenge
58
118
  webAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async (context, next) => {
59
119
  const rpID = context.hostname
60
120
 
@@ -67,10 +127,12 @@ webAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async (context
67
127
  context.body = options
68
128
  })
69
129
 
130
+ // Sign in with biometric authentication
70
131
  webAuthnGlobalPublicRouter.post(
71
- '/auth/signin-webauthn', createWebAuthnMiddleware('webauthn-login'),
132
+ '/auth/signin-webauthn',
133
+ createWebAuthnMiddleware('webauthn-login'),
72
134
  async (context, next) => {
73
- const { domain, user } = context. state
135
+ const { domain, user } = context.state
74
136
  const { request } = context
75
137
  const { body: reqBody } = request
76
138
 
@@ -79,9 +141,9 @@ webAuthnGlobalPublicRouter.post(
79
141
 
80
142
  var redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(reqBody.redirectTo || '/')}`
81
143
 
82
- /* 2단계 인터렉션 때문에 브라우저에서 fetch(...) 진행될 것이므로, redirect(3xx) 응답으로 처리할 없다. 따라서, 데이타로 redirectURL를 응답한다. */
144
+ /* Due to the two-step interaction, it will be processed by fetch(...) in the browser, so it cannot be handled with a redirect(3xx) response. Therefore, respond with redirectURL as data. */
83
145
  context.body = { redirectURL, verified: true }
84
146
 
85
- await next();
147
+ await next()
86
148
  }
87
149
  )
@@ -73,7 +73,10 @@ export class Appliance {
73
73
  ? 'longtext'
74
74
  : DATABASE_TYPE == 'oracle'
75
75
  ? 'clob'
76
- : 'varchar'
76
+ : DATABASE_TYPE == 'mssql'
77
+ ? 'nvarchar'
78
+ : 'varchar',
79
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
77
80
  })
78
81
  @Field({ nullable: true })
79
82
  @Directive('@privilege(category: "security", privilege: "query", domainOwnerGranted: true)')
@@ -99,7 +99,10 @@ export class Application {
99
99
  ? 'longtext'
100
100
  : DATABASE_TYPE == 'oracle'
101
101
  ? 'clob'
102
- : 'varchar'
102
+ : DATABASE_TYPE == 'mssql'
103
+ ? 'nvarchar'
104
+ : 'varchar',
105
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
103
106
  })
104
107
  @Field({ nullable: true })
105
108
  @Directive('@privilege(category: "security", privilege: "query", domainOwnerGranted: true)')
@@ -115,8 +118,14 @@ export class Application {
115
118
  ? 'enum'
116
119
  : DATABASE_TYPE == 'oracle'
117
120
  ? 'varchar2'
118
- : 'smallint',
119
- enum: ApplicationType,
121
+ : DATABASE_TYPE == 'mssql'
122
+ ? 'nvarchar'
123
+ : 'varchar',
124
+ enum:
125
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
126
+ ? ApplicationType
127
+ : undefined,
128
+ length: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? undefined : 32,
120
129
  default: ApplicationType.OTHERS
121
130
  })
122
131
  @Field()
@@ -2,7 +2,19 @@ import crypto from 'crypto'
2
2
  import jwt from 'jsonwebtoken'
3
3
  import { Directive, Field, ID, ObjectType } from 'type-graphql'
4
4
  import { GraphQLEmailAddress } from 'graphql-scalars'
5
- import { Column, CreateDateColumn, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'
5
+ import {
6
+ Column,
7
+ CreateDateColumn,
8
+ Entity,
9
+ Index,
10
+ JoinTable,
11
+ ManyToMany,
12
+ ManyToOne,
13
+ OneToMany,
14
+ PrimaryGeneratedColumn,
15
+ RelationId,
16
+ UpdateDateColumn
17
+ } from 'typeorm'
6
18
 
7
19
  import { config } from '@things-factory/env'
8
20
  import { Domain, getRepository } from '@things-factory/shell'
@@ -58,7 +70,15 @@ export class User {
58
70
  @Directive('@privilege(category: "security", privilege: "query", domainOwnerGranted: true)')
59
71
  @Column({
60
72
  nullable: true,
61
- type: DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? 'longtext' : DATABASE_TYPE == 'oracle' ? 'clob' : 'varchar'
73
+ type:
74
+ DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
75
+ ? 'longtext'
76
+ : DATABASE_TYPE == 'oracle'
77
+ ? 'clob'
78
+ : DATABASE_TYPE == 'mssql'
79
+ ? 'nvarchar'
80
+ : 'varchar',
81
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
62
82
  })
63
83
  password: string
64
84
 
@@ -89,8 +109,17 @@ export class User {
89
109
  ssoId: string
90
110
 
91
111
  @Column({
92
- type: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? 'enum' : DATABASE_TYPE == 'oracle' ? 'varchar2' : 'smallint',
93
- enum: UserStatus,
112
+ type:
113
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
114
+ ? 'enum'
115
+ : DATABASE_TYPE == 'oracle'
116
+ ? 'varchar2'
117
+ : DATABASE_TYPE == 'mssql'
118
+ ? 'nvarchar'
119
+ : 'varchar',
120
+ enum:
121
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? UserStatus : undefined,
122
+ length: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? undefined : 32,
94
123
  default: UserStatus.INACTIVE
95
124
  })
96
125
  @Field(type => String)
@@ -30,9 +30,15 @@ export class VerificationToken {
30
30
  DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
31
31
  ? 'enum'
32
32
  : DATABASE_TYPE == 'oracle'
33
- ? 'varchar2'
34
- : 'smallint',
35
- enum: VerificationTokenType,
33
+ ? 'varchar2'
34
+ : DATABASE_TYPE == 'mssql'
35
+ ? 'nvarchar'
36
+ : 'varchar',
37
+ enum:
38
+ DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
39
+ ? VerificationTokenType
40
+ : undefined,
41
+ length: DATABASE_TYPE == 'postgres' || DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb' ? undefined : 32,
36
42
  default: VerificationTokenType.ACTIVATION
37
43
  })
38
44
  @Field()
@@ -0,0 +1,29 @@
1
+ import { Domain, getRepository } from '@things-factory/shell'
2
+
3
+ import { User } from '../service/user/user'
4
+ import { Role } from 'service'
5
+
6
+ /**
7
+ * @description 사용자가 특정 도메인 또는 상위 도메인에서 특정 역할을 가지고 있는지 확인합니다.
8
+ *
9
+ * @param roleId 확인할 역할의 ID
10
+ * @param domain 역할을 확인할 도메인
11
+ * @param user 역할을 확인할 사용자
12
+ *
13
+ * @returns 사용자가 도메인 또는 상위 도메인에서 역할을 가지고 있는지 여부를 나타내는 boolean을 반환하는 Promise
14
+ */
15
+ export async function checkUserHasRole(roleId: string, domain: Domain, user: User): Promise<Boolean> {
16
+ if (!roleId) {
17
+ return true
18
+ }
19
+
20
+ const me = await getRepository(User).findOne({
21
+ where: { id: user.id },
22
+ relations: ['roles']
23
+ })
24
+
25
+ return me.roles
26
+ .filter(role => role.domainId === domain.id || (domain.parentId && role.domainId === domain.parentId))
27
+ .map(role => role.id)
28
+ .includes(roleId)
29
+ }