Skip to content

Commit

Permalink
Support Google Signin (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
byn9826 authored Aug 18, 2024
1 parent c01af0d commit f0f0fd7
Show file tree
Hide file tree
Showing 26 changed files with 364 additions and 108 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Features Supported
- OAuth 2.0 Support (Authorize, Token Exchange, Token Revoke, App Consent, App Scopes, RSA256 based JWT Authentication)
- User Authorization (Sign In, Sign Up, Sign Out, Email Verification, Password Reset, Email MFA, OTP MFA, MFA Enrollment, Brute-force Protection, Role-Based Access Control, Localization) [Screenshots](https://auth.valuemelody.com/screenshots.html#identity-pages-and-emails)
- User Authorization (Sign In, Sign Up, Google Sign In, Sign Out, Email Verification, Password Reset, Email MFA, OTP MFA, MFA Enrollment, Brute-force Protection, Role-Based Access Control, Localization) [Screenshots](https://auth.valuemelody.com/screenshots.html#identity-pages-and-emails)
- S2S REST API & Admin Panel (Manage Users, Manage Apps, Manage Scopes, Manage Roles) [Screenshots](https://auth.valuemelody.com/screenshots.html#admin-panel-pages)

## Why Melody Auth?
Expand Down
4 changes: 4 additions & 0 deletions admin-panel/app/[lang]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ const Page = () => {
<Table.Cell>AUTH_SERVER_URL</Table.Cell>
<Table.Cell>{configs.AUTH_SERVER_URL}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>GOOGLE_AUTH_CLIENT_ID</Table.Cell>
<Table.Cell>{configs.GOOGLE_AUTH_CLIENT_ID}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>SUPPORTED_LOCALES</Table.Cell>
<Table.Cell>{configs.SUPPORTED_LOCALES.join(', ')}</Table.Cell>
Expand Down
114 changes: 62 additions & 52 deletions admin-panel/app/[lang]/users/[authId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,66 +283,76 @@ const Page = () => {
</div>
</Table.Cell>
<Table.Cell>
<div className='flex items-center gap-4'>
{user.isActive && !isEmailEnrolled && (
<Button
size='xs'
onClick={handleEnrollEmailMfa}>
{t('users.enrollMfa')}
</Button>
)}
{user.isActive && isEmailEnrolled && !configs.EMAIL_MFA_IS_REQUIRED && (
{!user.googleId && (
<div className='flex items-center gap-4'>
{user.isActive && !isEmailEnrolled && (
<Button
size='xs'
onClick={handleEnrollEmailMfa}>
{t('users.enrollMfa')}
</Button>
)}
{user.isActive && isEmailEnrolled && !configs.EMAIL_MFA_IS_REQUIRED && (
<Button
size='xs'
onClick={handleResetEmailMfa}>
{t('users.resetMfa')}
</Button>
)}
{configs.ENABLE_EMAIL_VERIFICATION && user.isActive && !user.emailVerified && !emailResent && (
<Button
size='xs'
onClick={handleResendVerifyEmail}>
{t('users.resend')}
</Button>
)}
{configs.ENABLE_EMAIL_VERIFICATION && user.isActive && !user.emailVerified && emailResent && (
<div className='flex'>
<Badge>{t('users.sent')}</Badge>
</div>
)}
</div>
)}
</Table.Cell>
</Table.Row>
{user.googleId && (
<Table.Row>
<Table.Cell>{t('users.social')}</Table.Cell>
<Table.Cell>Google: {user.googleId}</Table.Cell>
</Table.Row>
)}
{!user.googleId && (
<Table.Row>
<Table.Cell>{t('users.authenticator')}</Table.Cell>
<TableCell>
<div className='flex'>
{isOtpEnrolled && (
<Badge color='gray'>{t('users.otpMfaEnrolled')}</Badge>
)}
{isOtpEnrolled && user.otpVerified && (
<Badge color='success'>{t('users.otpMfaVerified')}</Badge>
)}
</div>
</TableCell>
<TableCell>
{(user.otpVerified || user.mfaTypes.includes('otp')) && user.isActive && (
<Button
size='xs'
onClick={handleResetEmailMfa}>
onClick={handleResetOtpMfa}
>
{t('users.resetMfa')}
</Button>
)}
{configs.ENABLE_EMAIL_VERIFICATION && user.isActive && !user.emailVerified && !emailResent && (
{user.isActive && !isOtpEnrolled && (
<Button
size='xs'
onClick={handleResendVerifyEmail}>
{t('users.resend')}
onClick={handleEnrollOtpMfa}>
{t('users.enrollMfa')}
</Button>
)}
{configs.ENABLE_EMAIL_VERIFICATION && user.isActive && !user.emailVerified && emailResent && (
<div className='flex'>
<Badge>{t('users.sent')}</Badge>
</div>
)}
</div>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>{t('users.authenticator')}</Table.Cell>
<TableCell>
<div className='flex'>
{isOtpEnrolled && (
<Badge color='gray'>{t('users.otpMfaEnrolled')}</Badge>
)}
{isOtpEnrolled && user.otpVerified && (
<Badge color='success'>{t('users.otpMfaVerified')}</Badge>
)}
</div>
</TableCell>
<TableCell>
{(user.otpVerified || user.mfaTypes.includes('otp')) && user.isActive && (
<Button
size='xs'
onClick={handleResetOtpMfa}
>
{t('users.resetMfa')}
</Button>
)}
{user.isActive && !isOtpEnrolled && (
<Button
size='xs'
onClick={handleEnrollOtpMfa}>
{t('users.enrollMfa')}
</Button>
)}
</TableCell>
</Table.Row>
</TableCell>
</Table.Row>
)}
<Table.Row>
<Table.Cell>{t('users.locale')}</Table.Cell>
<Table.Cell>
Expand Down Expand Up @@ -379,7 +389,7 @@ const Page = () => {
)}
</Table.Cell>
</Table.Row>
{enableAccountLock && (
{!user.googleId && enableAccountLock && (
<Table.Row>
<Table.Cell>{t('users.lockedIPs')}</Table.Cell>
<Table.Cell>
Expand Down
1 change: 1 addition & 0 deletions admin-panel/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
},
"users": {
"title": "Users",
"social": "Social Sign In",
"user": "User",
"authId": "Auth ID",
"email": "Email",
Expand Down
1 change: 1 addition & 0 deletions admin-panel/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
},
"users": {
"title": "Utilisateurs",
"social": "Connexion via un réseau social",
"user": "Utilisateur",
"authId": "ID Auth",
"email": "Email",
Expand Down
4 changes: 4 additions & 0 deletions docs/auth-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ npm run prod:deploy
- **Default:** https://raw.githubusercontent.com/ValueMelody/melody-homepage/main/logo.jpg
- **Description:** The logo used for branding.

### GOOGLE_AUTH_CLIENT_ID
- **Default:** ""
- **Description:** The Google Authentication Client ID is required to enable the Google Sign-In function. This ID is obtained from the Google Developer Console and uniquely identifies your application to Google. If this value is left empty, the Google Sign-In button will be suppressed and the sign-in functionality will not be available.

### ENABLE_SIGN_UP
- **Default:** true
- **Description:** Determines if user sign-up is allowed. If set to false, the sign-up button will be suppressed on the sign-in page.
Expand Down
1 change: 1 addition & 0 deletions server/migrations/0014_add_google_id_to_user_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE user ADD googleId text DEFAULT null;
4 changes: 4 additions & 0 deletions server/src/configs/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export const requestError = Object.freeze({
en: 'No user found.',
fr: 'Aucun utilisateur trouvé.',
},
disabledUser: {
en: 'This account has been disabled.',
fr: 'Ce compte a été désactivé.',
},
accountLocked: {
en: 'Account temporarily locked due to excessive login failures.',
fr: 'Compte temporairement bloqué en raison de trop nombreuses tentatives de connexion échouées.',
Expand Down
1 change: 1 addition & 0 deletions server/src/configs/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type Bindings = {
ENABLE_EMAIL_VERIFICATION: boolean;
EMAIL_MFA_IS_REQUIRED: boolean;
OTP_MFA_IS_REQUIRED: boolean;
GOOGLE_AUTH_CLIENT_ID: string;
ENFORCE_ONE_MFA_ENROLLMENT: boolean;
ALLOW_EMAIL_MFA_AS_BACKUP: boolean;
ACCOUNT_LOCKOUT_THRESHOLD: number;
Expand Down
25 changes: 14 additions & 11 deletions server/src/dtos/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import {
} from 'utils'
import { userModel } from 'models'

export class PostAuthorizeSocialSignInReqDto extends oauthDto.GetAuthorizeReqDto {
@IsString()
@IsNotEmpty()
credential: string

constructor (dto: PostAuthorizeSocialSignInReqDto) {
super(dto)
this.credential = dto.credential
}
}

export class PostAuthorizeReqWithPasswordDto extends oauthDto.GetAuthorizeReqDto {
@IsEmail()
email: string
Expand Down Expand Up @@ -73,7 +84,7 @@ export class PostAuthorizeReqWithRequiredNamesDto extends PostAuthorizeReqWithPa
}
}

export class PostAuthorizeConsentReqDto {
export class GetAuthorizeFollowUpReqDto {
@IsString()
@IsNotEmpty()
state: string
Expand All @@ -97,16 +108,6 @@ export class PostAuthorizeConsentReqDto {
}
}

export class GetAuthorizeFollowUpReqDto extends PostAuthorizeConsentReqDto {
@IsString()
locale: typeConfig.Locale

constructor (dto: GetAuthorizeFollowUpReqDto) {
super(dto)
this.locale = dto.locale
}
}

export const parseGetAuthorizeFollowUpReq = async (c: Context<typeConfig.Context>) => {
const queryDto = new GetAuthorizeFollowUpReqDto({
state: c.req.query('state') ?? '',
Expand All @@ -121,6 +122,8 @@ export const parseGetAuthorizeFollowUpReq = async (c: Context<typeConfig.Context
return queryDto
}

export class PostAuthorizeConsentReqDto extends GetAuthorizeFollowUpReqDto {}

export class PostAuthorizeResendEmailMfaDto {
@IsString()
@IsNotEmpty()
Expand Down
67 changes: 64 additions & 3 deletions server/src/handlers/identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
identityDto, oauthDto,
} from 'dtos'
import {
appService, consentService, emailService, kvService, scopeService, sessionService, userService,
appService, consentService, emailService, jwtService, kvService, scopeService, sessionService, userService,
} from 'services'
import {
formatUtil, validateUtil,
Expand All @@ -25,6 +25,7 @@ import { userModel } from 'models'
enum AuthorizeStep {
Account = 0,
Password = 0,
Google = 0,
Consent = 1,
MfaEnroll = 2,
OtpMfa = 3,
Expand All @@ -43,6 +44,8 @@ const handlePostAuthorize = async (
authCodeBody.appId,
)

const isSocialLogin = !!authCodeBody.user.googleId

const {
EMAIL_MFA_IS_REQUIRED: enableEmailMfa,
OTP_MFA_IS_REQUIRED: enableOtpMfa,
Expand All @@ -51,15 +54,22 @@ const handlePostAuthorize = async (

const requireMfaEnroll =
step < 2 &&
!isSocialLogin &&
enforceMfa &&
!enableEmailMfa &&
!enableOtpMfa &&
!authCodeBody.user.mfaTypes.length

const requireOtpMfa = step < 3 && (enableOtpMfa || authCodeBody.user.mfaTypes.includes(userModel.MfaType.Otp))
const requireOtpMfa =
step < 3 &&
!isSocialLogin &&
(enableOtpMfa || authCodeBody.user.mfaTypes.includes(userModel.MfaType.Otp))
const requireOtpSetup = requireOtpMfa && !authCodeBody.user.otpVerified

const requireEmailMfa = step < 4 && (enableEmailMfa || authCodeBody.user.mfaTypes.includes(userModel.MfaType.Email))
const requireEmailMfa =
step < 4 &&
!isSocialLogin &&
(enableEmailMfa || authCodeBody.user.mfaTypes.includes(userModel.MfaType.Email))

if (!requireConsent && !requireMfaEnroll && !requireOtpMfa && !requireEmailMfa) {
sessionService.setAuthInfoSession(
Expand Down Expand Up @@ -145,6 +155,7 @@ export const getAuthorizePassword = async (c: Context<typeConfig.Context>) => {
ENABLE_PASSWORD_RESET: enablePasswordReset,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
GOOGLE_AUTH_CLIENT_ID: googleClientId,
} = env(c)

const queryString = formatUtil.getQueryString(c)
Expand All @@ -156,6 +167,7 @@ export const getAuthorizePassword = async (c: Context<typeConfig.Context>) => {
logoUrl={logoUrl}
enableSignUp={enableSignUp}
enablePasswordReset={enablePasswordReset}
googleClientId={googleClientId}
/>)
}

Expand Down Expand Up @@ -618,6 +630,55 @@ export const postResendEmailMfa = async (c: Context<typeConfig.Context>) => {
return c.json({ success: true })
}

export const postAuthorizeGoogle = async (c: Context<typeConfig.Context>) => {
const reqBody = await c.req.json()

const bodyDto = new identityDto.PostAuthorizeSocialSignInReqDto({
...reqBody,
scopes: reqBody.scope.split(' '),
})
await validateUtil.dto(bodyDto)

const app = await appService.verifySPAClientRequest(
c,
bodyDto.clientId,
bodyDto.redirectUri,
)

const googleUser = await jwtService.verifyGoogleCredential(bodyDto.credential)
if (!googleUser) throw new errorConfig.NotFound(localeConfig.Error.NoUser)

const user = await userService.processGoogleAccount(
c,
googleUser,
bodyDto.locale,
)

const { AUTHORIZATION_CODE_EXPIRES_IN: codeExpiresIn } = env(c)

const authCode = genRandomString(128)
const request = new oauthDto.GetAuthorizeReqDto(bodyDto)
const authCodeBody = {
appId: app.id,
appName: app.name,
user,
request,
}
await kvService.storeAuthCode(
c.env.KV,
authCode,
authCodeBody,
codeExpiresIn,
)

return handlePostAuthorize(
c,
AuthorizeStep.Google,
authCode,
authCodeBody,
)
}

export const postAuthorizePassword = async (c: Context<typeConfig.Context>) => {
const reqBody = await c.req.json()

Expand Down
Loading

0 comments on commit f0f0fd7

Please sign in to comment.