@things-factory/auth-base 7.0.1-rc.1 → 7.0.1-rc.10
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/client/actions/auth.ts +3 -3
- package/dist-client/actions/auth.d.ts +3 -3
- package/dist-client/actions/auth.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/constants/error-code.d.ts +1 -1
- package/dist-server/constants/error-code.js +2 -2
- package/dist-server/constants/error-code.js.map +1 -1
- package/dist-server/middlewares/webauthn-middleware.d.ts +1 -2
- package/dist-server/middlewares/webauthn-middleware.js +74 -47
- package/dist-server/middlewares/webauthn-middleware.js.map +1 -1
- package/dist-server/router/webauthn-router.js +50 -26
- package/dist-server/router/webauthn-router.js.map +1 -1
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/server/constants/error-code.ts +1 -1
- package/server/middlewares/webauthn-middleware.ts +98 -55
- package/server/router/webauthn-router.ts +70 -38
- package/server/service/web-auth-credential/web-auth-credential.ts +1 -0
- package/translations/en.json +3 -2
- package/translations/ja.json +3 -2
- package/translations/ko.json +3 -1
- package/translations/ms.json +3 -2
- package/translations/zh.json +4 -2
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@things-factory/auth-base",
|
3
|
-
"version": "7.0.1-rc.
|
3
|
+
"version": "7.0.1-rc.10",
|
4
4
|
"main": "dist-server/index.js",
|
5
5
|
"browser": "dist-client/index.js",
|
6
6
|
"things-factory": true,
|
@@ -30,20 +30,21 @@
|
|
30
30
|
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create -d ./server/migrations"
|
31
31
|
},
|
32
32
|
"dependencies": {
|
33
|
-
"@
|
34
|
-
"@
|
35
|
-
"@things-factory/
|
36
|
-
"@things-factory/
|
33
|
+
"@simplewebauthn/browser": "^10.0.0",
|
34
|
+
"@simplewebauthn/server": "^10.0.0",
|
35
|
+
"@things-factory/email-base": "^7.0.1-rc.10",
|
36
|
+
"@things-factory/env": "^7.0.1-rc.8",
|
37
|
+
"@things-factory/shell": "^7.0.1-rc.10",
|
38
|
+
"@things-factory/utils": "^7.0.1-rc.7",
|
37
39
|
"@types/webappsec-credential-management": "^0.6.8",
|
38
40
|
"jsonwebtoken": "^9.0.0",
|
39
41
|
"koa-passport": "^6.0.0",
|
40
42
|
"koa-session": "^6.4.0",
|
41
43
|
"oauth2orize-koa": "^1.3.2",
|
42
44
|
"passport": "^0.7.0",
|
43
|
-
"passport-
|
45
|
+
"passport-custom": "^1.1.1",
|
44
46
|
"passport-jwt": "^4.0.0",
|
45
|
-
"passport-local": "^1.0.0"
|
46
|
-
"popsicle-cookie-jar": "^1.0.0"
|
47
|
+
"passport-local": "^1.0.0"
|
47
48
|
},
|
48
|
-
"gitHead": "
|
49
|
+
"gitHead": "259524f7aeb4368a5a4792aa31e4d11b525dbb28"
|
49
50
|
}
|
@@ -15,6 +15,6 @@ 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 AUTHN_VERIFICATION_FAILED = 'authn verification failed'
|
18
19
|
export const USER_CREDENTIAL_NOT_FOUND = 'user credential not found'
|
19
20
|
export const AUTH_ERROR = 'auth error'
|
20
|
-
export const FIDO2_CERT_UNSUPPORTED = 'fido2 certificate unsupported'
|
@@ -1,80 +1,123 @@
|
|
1
1
|
import passport from 'koa-passport'
|
2
|
-
import { Strategy as
|
2
|
+
import { Strategy as CustomStrategy } from 'passport-custom'
|
3
3
|
|
4
4
|
import { getRepository } from '@things-factory/shell'
|
5
5
|
|
6
6
|
import { User } from '../service/user/user'
|
7
7
|
import { AuthError } from '../errors/auth-error'
|
8
|
+
|
8
9
|
import { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential'
|
10
|
+
import { verifyRegistrationResponse, verifyAuthenticationResponse } from '@simplewebauthn/server'
|
9
11
|
|
10
|
-
|
12
|
+
import { AuthenticatorAssertionResponse } from '@simplewebauthn/types'
|
11
13
|
|
12
14
|
passport.use(
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
15
|
+
'webauthn-register',
|
16
|
+
new CustomStrategy(async (context, done) => {
|
17
|
+
const { body, session, user, hostname, origin } = context as any
|
18
|
+
|
19
|
+
const challenge = session.challenge
|
20
|
+
|
21
|
+
const verification = await verifyRegistrationResponse({
|
22
|
+
response: body,
|
23
|
+
expectedChallenge: challenge,
|
24
|
+
expectedOrigin: origin,
|
25
|
+
expectedRPID: hostname,
|
26
|
+
expectedType: 'webauthn.create',
|
27
|
+
requireUserVerification: false
|
28
|
+
})
|
29
|
+
|
30
|
+
if (verification.verified) {
|
31
|
+
const { registrationInfo } = verification
|
32
|
+
const publicKey = Buffer.from(registrationInfo.credentialPublicKey).toString('base64')
|
26
33
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
34
|
+
if (user) {
|
35
|
+
const webAuthRepository = getRepository(WebAuthCredential)
|
36
|
+
await webAuthRepository.save({
|
37
|
+
user,
|
38
|
+
credentialId: registrationInfo.credentialID,
|
39
|
+
publicKey,
|
40
|
+
counter: registrationInfo.counter,
|
41
|
+
creator: user,
|
42
|
+
updater: user
|
43
|
+
})
|
32
44
|
}
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
+
|
46
|
+
return done(null, user)
|
47
|
+
} else {
|
48
|
+
return done(null, false)
|
49
|
+
}
|
50
|
+
})
|
51
|
+
)
|
52
|
+
|
53
|
+
passport.use(
|
54
|
+
'webauthn-login',
|
55
|
+
new CustomStrategy(async (context, done) => {
|
56
|
+
const { body, session, origin, hostname } = context as any
|
57
|
+
|
58
|
+
const challenge = session.challenge
|
59
|
+
|
60
|
+
const assertionResponse = body as {
|
61
|
+
id: string
|
62
|
+
response: AuthenticatorAssertionResponse
|
63
|
+
}
|
64
|
+
|
65
|
+
const credential = await getRepository(WebAuthCredential).findOne({
|
66
|
+
where: {
|
67
|
+
credentialId: assertionResponse.id
|
68
|
+
},
|
69
|
+
relations: ['user']
|
70
|
+
})
|
71
|
+
|
72
|
+
if (!credential) {
|
73
|
+
return done(null, false)
|
74
|
+
}
|
75
|
+
|
76
|
+
const verification = await verifyAuthenticationResponse({
|
77
|
+
response: body,
|
78
|
+
expectedChallenge: challenge,
|
79
|
+
expectedOrigin: origin,
|
80
|
+
expectedRPID: hostname,
|
81
|
+
requireUserVerification: false,
|
82
|
+
authenticator: {
|
83
|
+
credentialID: credential.credentialId,
|
84
|
+
credentialPublicKey: new Uint8Array(Buffer.from(credential.publicKey, 'base64')),
|
85
|
+
counter: credential.counter
|
45
86
|
}
|
87
|
+
})
|
46
88
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
counter: 0
|
52
|
-
})
|
89
|
+
if (verification.verified) {
|
90
|
+
const { authenticationInfo } = verification
|
91
|
+
credential.counter = authenticationInfo.newCounter
|
92
|
+
await getRepository(WebAuthCredential).save(credential)
|
53
93
|
|
54
|
-
|
94
|
+
const user = credential.user
|
95
|
+
return done(null, user)
|
96
|
+
} else {
|
97
|
+
return done(verification, false)
|
55
98
|
}
|
56
|
-
)
|
99
|
+
})
|
57
100
|
)
|
58
101
|
|
59
|
-
export
|
60
|
-
return
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
if (
|
66
|
-
throw new AuthError(info)
|
67
|
-
} else {
|
102
|
+
export function createWebAuthnMiddleware(strategy: 'webauthn-register' | 'webauthn-login') {
|
103
|
+
return async function webAuthnMiddleware(context, next) {
|
104
|
+
return passport.authenticate(
|
105
|
+
strategy,
|
106
|
+
{ session: true, failureMessage: true, failWithError: true },
|
107
|
+
async (err, user) => {
|
108
|
+
if (err || !user) {
|
68
109
|
throw new AuthError({
|
69
|
-
errorCode: AuthError.ERROR_CODES.
|
70
|
-
detail:
|
110
|
+
errorCode: AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED,
|
111
|
+
detail: err
|
71
112
|
})
|
113
|
+
} else {
|
114
|
+
context.state.user = user
|
115
|
+
|
116
|
+
context.body = { user, verified: true }
|
72
117
|
}
|
73
|
-
} else {
|
74
|
-
context.state.user = user
|
75
118
|
|
76
119
|
await next()
|
77
120
|
}
|
78
|
-
|
79
|
-
|
121
|
+
)(context, next)
|
122
|
+
}
|
80
123
|
}
|
@@ -1,55 +1,87 @@
|
|
1
|
-
import util from 'util'
|
2
1
|
import Router from 'koa-router'
|
2
|
+
import { getRepository } from '@things-factory/shell'
|
3
|
+
import { appPackage } from '@things-factory/env'
|
3
4
|
|
5
|
+
import { generateRegistrationOptions, generateAuthenticationOptions } from '@simplewebauthn/server'
|
6
|
+
|
7
|
+
import { WebAuthCredential } from '../service/web-auth-credential/web-auth-credential'
|
8
|
+
import {
|
9
|
+
PublicKeyCredentialCreationOptionsJSON,
|
10
|
+
} from '@simplewebauthn/server/script/deps'
|
4
11
|
import { setAccessTokenCookie } from '../utils/access-token-cookie'
|
5
|
-
import {
|
12
|
+
import { createWebAuthnMiddleware } from '../middlewares/webauthn-middleware';
|
6
13
|
|
7
14
|
export const webAuthnGlobalPublicRouter = new Router()
|
8
15
|
export const webAuthnGlobalPrivateRouter = new Router()
|
9
16
|
|
10
|
-
const
|
17
|
+
const { name: rpName } = appPackage as any
|
11
18
|
|
12
|
-
|
13
|
-
const
|
19
|
+
webAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async (context, next) => {
|
20
|
+
const { user } = context.state
|
21
|
+
const rpID = context.hostname
|
14
22
|
|
15
|
-
|
16
|
-
|
17
|
-
|
23
|
+
const webAuthCredentials = await getRepository(WebAuthCredential).find({
|
24
|
+
where: {
|
25
|
+
user: { id: user.id }
|
26
|
+
}
|
27
|
+
})
|
28
|
+
|
29
|
+
const options: PublicKeyCredentialCreationOptionsJSON = await generateRegistrationOptions({
|
30
|
+
rpName,
|
31
|
+
rpID,
|
32
|
+
userName: user.email,
|
33
|
+
userDisplayName: user.name,
|
34
|
+
// Don't prompt users for additional information about the authenticator
|
35
|
+
// (Recommended for smoother UX)
|
36
|
+
attestationType: 'none',
|
37
|
+
// Prevent users from re-registering existing authenticators
|
38
|
+
excludeCredentials: webAuthCredentials.map(credential => ({
|
39
|
+
id: credential.credentialId
|
40
|
+
// Optional
|
41
|
+
// transports: credential.transports
|
42
|
+
})),
|
43
|
+
authenticatorSelection: {
|
44
|
+
// Defaults
|
45
|
+
residentKey: 'preferred',
|
46
|
+
userVerification: 'preferred',
|
47
|
+
// Optional
|
48
|
+
authenticatorAttachment: 'platform'
|
49
|
+
}
|
50
|
+
})
|
51
|
+
|
52
|
+
context.session.challenge = options.challenge
|
53
|
+
context.body = options
|
18
54
|
})
|
19
55
|
|
20
|
-
|
21
|
-
const { domain, user } = context.state
|
22
|
-
const { request } = context
|
23
|
-
const { body: reqBody } = request
|
56
|
+
webAuthnGlobalPrivateRouter.post('/auth/verify-registration', createWebAuthnMiddleware('webauthn-register'));
|
24
57
|
|
25
|
-
|
26
|
-
|
58
|
+
webAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async (context, next) => {
|
59
|
+
const rpID = context.hostname
|
27
60
|
|
28
|
-
|
61
|
+
const options = await generateAuthenticationOptions({
|
62
|
+
rpID,
|
63
|
+
userVerification: 'preferred'
|
64
|
+
})
|
29
65
|
|
30
|
-
|
31
|
-
context.body =
|
66
|
+
context.session.challenge = options.challenge
|
67
|
+
context.body = options
|
32
68
|
})
|
33
69
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
{
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
}
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
name: name,
|
51
|
-
displayName: name
|
52
|
-
},
|
53
|
-
challenge: Buffer.from(challenge).toString('base64')
|
70
|
+
webAuthnGlobalPublicRouter.post(
|
71
|
+
'/auth/signin-webauthn', createWebAuthnMiddleware('webauthn-login'),
|
72
|
+
async (context, next) => {
|
73
|
+
const { domain, user } = context. state
|
74
|
+
const { request } = context
|
75
|
+
const { body: reqBody } = request
|
76
|
+
|
77
|
+
const token = await user.sign({ subdomain: domain?.subdomain })
|
78
|
+
setAccessTokenCookie(context, token)
|
79
|
+
|
80
|
+
var redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(reqBody.redirectTo || '/')}`
|
81
|
+
|
82
|
+
/* 2단계 인터렉션 때문에 브라우저에서 fetch(...)로 진행될 것이므로, redirect(3xx) 응답으로 처리할 수 없다. 따라서, 데이타로 redirectURL를 응답한다. */
|
83
|
+
context.body = { redirectURL, verified: true }
|
84
|
+
|
85
|
+
await next();
|
54
86
|
}
|
55
|
-
|
87
|
+
)
|
package/translations/en.json
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"error.auth error": "auth error. {message}",
|
3
|
+
"error.authn verification failed": "user credential verification failed.",
|
3
4
|
"error.confirm password not matched": "new password and confirm password is not matched",
|
4
5
|
"error.domain mismatch": "certificate is not for this domain",
|
5
6
|
"error.domain not allowed": "user not allowed domain `{subdomain}`",
|
6
7
|
"error.failed to find x": "failed to find {x}",
|
7
|
-
"error.fido2 certificate unsupported": "FIDO2 certificate unsupported",
|
8
8
|
"error.password should match the rule": "password should match following rule. ${rule}",
|
9
9
|
"error.password used in the past": "password used in the past",
|
10
10
|
"error.subdomain not found": "domain not found",
|
11
11
|
"error.token or password is invalid": "token or password is invalid",
|
12
12
|
"error.unavailable-domain": "unavailable domain",
|
13
|
-
"error.user credential
|
13
|
+
"error.user credential registeration failed": "user credential registration failed. It may be an already registered credential.",
|
14
|
+
"error.user credential registration not allowed": "user credential registration failed. The registration timed out or was not allowed.",
|
14
15
|
"error.user duplicated": "user duplicated",
|
15
16
|
"error.user not activated": "user is not activated",
|
16
17
|
"error.user not found": "user not found",
|
package/translations/ja.json
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"error.auth error": "認証エラー。{message}",
|
3
|
+
"error.authn verification failed": "ユーザー認証に失敗しました。",
|
3
4
|
"error.confirm password not matched": "新しいパスワードと確認パスワードが一致しません.",
|
4
5
|
"error.domain mismatch": "証明書のドメインと現在のドメインが一致しません.",
|
5
6
|
"error.domain not allowed": "'{subdomain}' 領域はこのユーザに許可されていません.",
|
6
7
|
"error.failed to find x": "{x}が見つかりません.",
|
7
|
-
"error.fido2 certificate unsupported": "fido2証明書はサポートされていません",
|
8
8
|
"error.password should match the rule": "パスワードは次の規則を守らなければなりません. {rule}",
|
9
9
|
"error.password used in the past": "過去に使用されたパスワードです.",
|
10
10
|
"error.subdomain not found": "サブドメインが見つかりません.",
|
11
11
|
"error.token or password is invalid": "トークンまたはパスワードが無効です.",
|
12
12
|
"error.unavailable-domain": "使用できないドメインです.",
|
13
|
-
"error.user credential
|
13
|
+
"error.user credential registeration failed": "ユーザー資格情報の登録に失敗しました。既に登録されている資格情報の可能性があります。",
|
14
|
+
"error.user credential registration not allowed": "ユーザー資格情報の登録に失敗しました。登録のタイムアウトまたは登録が許可されていません。",
|
14
15
|
"error.user duplicated": "同じメールで登録されたアカウントが存在します.",
|
15
16
|
"error.user not activated": "ユーザーがアクティブ化されていません.",
|
16
17
|
"error.user not found": "ユーザーが存在しません.",
|
package/translations/ko.json
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"error.auth error": "인증 오류. {message}",
|
3
|
+
"error.authn verification failed": "사용자 자격 증명을 실패하였습니다.",
|
3
4
|
"error.confirm password not matched": "새 비밀번호와 확인 비밀번호가 일치하지 않습니다.",
|
4
5
|
"error.domain mismatch": "인증서의 도메인과 현재 도메인이 일치하지 않습니다.",
|
5
6
|
"error.domain not allowed": "'{subdomain}' 영역은 이 사용자에게 허가되지 않았습니다.",
|
6
7
|
"error.failed to find x": "{x}을(를) 찾을 수 없습니다.",
|
7
|
-
"error.fido2 certificate unsupported": "제공된 인증서가 올바르지 않거나 지원되지 않는 형식입니다. 다른 로그인 방법을 사용하세요.",
|
8
8
|
"error.password should match the rule": "비밀번호는 다음 규칙을 지켜야 합니다. {rule}",
|
9
9
|
"error.password used in the past": "과거에 사용된 비밀번호입니다.",
|
10
10
|
"error.subdomain not found": "서브도메인을 찾을 수 없습니다.",
|
@@ -15,6 +15,8 @@
|
|
15
15
|
"error.user not activated": "사용자가 활성화되지 않았습니다.",
|
16
16
|
"error.user not found": "사용자가 존재하지 않습니다.",
|
17
17
|
"error.user or verification token not found": "사용자 또는 확인토큰을 찾을 수 없습니다.",
|
18
|
+
"error.user credential registeration failed": "사용자 인증서 등록이 실패하였습니다. 이미 등록된 인증서일 수 있습니다.",
|
19
|
+
"error.user credential registration not allowed": "사용자 인증서 등록이 실패하였습니다. 등록 시간이 초과되었거나 등록이 허용되지 않았습니다.",
|
18
20
|
"error.user validation failed": "사용자 확인에 실패하였습니다.",
|
19
21
|
"error.x is not a member of y": "{x}은(는) {y}의 멤버가 아닙니다.",
|
20
22
|
"field.active": "활성화",
|
package/translations/ms.json
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"error.auth error": "ralat pengesahan. {message}",
|
3
|
+
"error.authn verification failed": "pengesahan kelayakan pengguna gagal.",
|
3
4
|
"error.confirm password not matched": "Kata laluan baru dan pengesahan kata laluan tidak sepadan",
|
4
5
|
"error.domain mismatch": "Sijil tidak sesuai untuk domain ini",
|
5
6
|
"error.domain not allowed": "Pengguna tidak dibenarkan domain `{subdomain}`",
|
6
7
|
"error.failed to find x": "Gagal mencari {x}",
|
7
|
-
"error.fido2 certificate unsupported": "sijil fido2 tidak disokong",
|
8
8
|
"error.password should match the rule": "Kata laluan harus mematuhi peraturan berikut. ${rule}",
|
9
9
|
"error.password used in the past": "Kata laluan telah digunakan dalam masa lampau",
|
10
10
|
"error.subdomain not found": "Domain tidak ditemui",
|
11
11
|
"error.token or password is invalid": "Token atau kata laluan tidak sah",
|
12
12
|
"error.unavailable-domain": "Domain tidak tersedia",
|
13
|
-
"error.user credential
|
13
|
+
"error.user credential registeration failed": "pendaftaran kelayakan pengguna gagal. Mungkin kelayakan tersebut sudah didaftarkan.",
|
14
|
+
"error.user credential registration not allowed": "pendaftaran kelayakan pengguna gagal. Masa pendaftaran telah tamat atau pendaftaran tidak dibenarkan.",
|
14
15
|
"error.user duplicated": "Emel telah digunakan oleh akaun lain",
|
15
16
|
"error.user not activated": "Pengguna tidak diaktifkan",
|
16
17
|
"error.user not found": "Pengguna tidak ditemui",
|
package/translations/zh.json
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
{
|
2
2
|
"error.auth error": "认证错误。{message}",
|
3
|
+
"error.authn verification failed": "用户认证失败。",
|
4
|
+
"error.user verification failed": "用户验证失败",
|
3
5
|
"error.confirm password not matched": "新密码与确认密码不匹配!",
|
4
6
|
"error.domain mismatch": "证书不适用于该域!",
|
5
7
|
"error.domain not allowed": "用户无权限使用`{subdomain}`域!",
|
6
8
|
"error.failed to find x": "查询{x}失败!",
|
7
|
-
"error.fido2 certificate unsupported": "fido2证书不支持",
|
8
9
|
"error.password should match the rule": "密码应符合以下规则。${rule}",
|
9
10
|
"error.password used in the past": "使用过的密码!",
|
10
11
|
"error.subdomain not found": "用户域查询失败!",
|
11
12
|
"error.token or password is invalid": "令牌或密码无效!",
|
12
13
|
"error.unavailable-domain": "不可用的域名",
|
13
|
-
"error.user credential
|
14
|
+
"error.user credential registeration failed": "用户凭证注册失败。可能是已注册的凭证。",
|
15
|
+
"error.user credential registration not allowed": "用户凭证注册失败。注册超时或注册不被允许。",
|
14
16
|
"error.user duplicated": "有一个用户帐户使用相同的电子邮件",
|
15
17
|
"error.user not activated": "用户未激活!",
|
16
18
|
"error.user not found": "找不到用户",
|