Skip to content

Commit

Permalink
feat: add oauth google
Browse files Browse the repository at this point in the history
  • Loading branch information
DeVoresyah committed Jan 4, 2024
1 parent 2bf1275 commit 7535e9b
Show file tree
Hide file tree
Showing 14 changed files with 504 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .adonisrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"./providers/AppProvider",
"@adonisjs/core",
"./providers/ResponseProvider",
"@adonisjs/lucid"
"@adonisjs/lucid",
"@adonisjs/ally"
],
"aceProviders": [
"@adonisjs/repl"
Expand Down
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ PG_USER=lucid
PG_PASSWORD=
PG_DB_NAME=lucid
SQLITE_LOCATION=
SENDGRID_API_KEY=
SENDGRID_API_KEY=
GOOGLE_CLIENT_ID=clientId
GOOGLE_CLIENT_SECRET=clientSecret
30 changes: 30 additions & 0 deletions ace-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,12 @@
"propertyName": "compactOutput",
"type": "boolean",
"description": "A compact single-line output"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
Expand Down Expand Up @@ -407,6 +413,12 @@
"propertyName": "compactOutput",
"type": "boolean",
"description": "A compact single-line output"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
Expand Down Expand Up @@ -457,6 +469,12 @@
"propertyName": "dryRun",
"type": "boolean",
"description": "Do not run actual queries. Instead view the SQL output"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
Expand Down Expand Up @@ -494,6 +512,12 @@
"propertyName": "seed",
"type": "boolean",
"description": "Run seeders"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
},
Expand Down Expand Up @@ -537,6 +561,12 @@
"propertyName": "dropTypes",
"type": "boolean",
"description": "Drop all custom types (Postgres only)"
},
{
"name": "disable-locks",
"propertyName": "disableLocks",
"type": "boolean",
"description": "Disable locks acquired to run migrations safely"
}
]
}
Expand Down
9 changes: 9 additions & 0 deletions app/Controllers/Http/v1/Auth/AuthsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,15 @@ export default class AuthsController {
return response.api({ message: 'Invalid credentials.' }, StatusCodes.UNAUTHORIZED)
}

if (!user.encryptedPassword) {
return response.api(
{
message:
'Please log in using the same single sign-on method you originally used, as no password is associated with your account.',
},
StatusCodes.UNAUTHORIZED
)
}
const isPasswordValid = await Hash.verify(user.encryptedPassword, payload.password)

if (!isPasswordValid) {
Expand Down
211 changes: 211 additions & 0 deletions app/Controllers/Http/v1/Auth/SsoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import Database from '@ioc:Adonis/Lucid/Database'
import Env from '@ioc:Adonis/Core/Env'
import Md5 from 'App/Helpers/Md5Helper'
import JwtService from 'App/Services/JwtService'
import StringTransform from 'App/Helpers/StringTransform'
import { cuid } from '@ioc:Adonis/Core/Helpers'
import { DateTime } from 'luxon'

// Services
import ResendService from 'App/Services/ResendService'

// Models
import User from 'App/Models/User'
import Identity from 'App/Models/Identity'
import Session from 'App/Models/Session'
import RefreshToken from 'App/Models/RefreshToken'

// Types
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class SsoController {
private jwt = new JwtService()
private md5 = new Md5()
private mailer = new ResendService()

public async redirect({ request, ally }: HttpContextContract) {
const { provider } = request.params()

return ally.use(provider).stateless().redirect()
}

public async callback({ request, response, ally }: HttpContextContract) {
const { provider } = request.params()
const headers = request.headers()
const ssoProvider = ally.use(provider).stateless()

if (ssoProvider.accessDenied()) {
return 'Access was denied'
}

if (ssoProvider.stateMisMatch()) {
return 'Request expired. Retry again'
}

if (ssoProvider.hasError()) {
return ssoProvider.getError()
}

const ssoUser = await ssoProvider.user()

const user = await User.findBy('email', ssoUser.email)

if (!user) {
const lastSignInAt = DateTime.now()
const confirmationToken = this.md5.generate(StringTransform.generateOtpNumber())

// Create new account
const result = await Database.transaction(async (trx) => {
const user = await User.create(
{
email: ssoUser.email,
encryptedPassword: null,
confirmationToken,
confirmationSentAt: lastSignInAt,
emailConfirmedAt:
ssoUser.emailVerificationState === 'verified' ? DateTime.now() : undefined,
rawAppMetaData: {
provider,
providers: [provider],
},
rawUserMetaData: ssoUser.original,
phone: null,
isSsoUser: true,
},
{ client: trx }
)

const identity = await Identity.create(
{
userId: user.id,
provider,
identity_data: ssoUser.original,
email: ssoUser.email,
},
{ client: trx }
)

return {
user,
identity,
}
})

if (ssoUser.emailVerificationState !== 'verified') {
this.mailer.sendVerification(ssoUser.email, confirmationToken, Env.get('APP_URL'))

return 'Account has been created, please check your email to verify your account.'
}

if (ssoUser.emailVerificationState === 'verified') {
const newSession = await Database.transaction(async (trx) => {
const session = await Session.create(
{
userId: result.user.id,
userAgent: headers['user-agent'],
ip: request.ips()[0],
},
{ client: trx }
)

const refreshToken = await RefreshToken.create(
{
userId: result.user.id,
sessionId: session.id,
token: cuid(),
revoked: false,
parent: null,
},
{ client: trx }
)

return {
session,
refreshToken,
}
})

result.user.lastSignInAt = lastSignInAt
await result.user.save()

result.identity.lastSignInAt = lastSignInAt
await result.identity.save()

const userToken = this.jwt
.generate({ user_id: result.user.id, session_id: newSession.session.id })
.make()
const expiresAt = DateTime.now().plus({ days: 7 }).toUnixInteger()

return response.redirect(
`${Env.get('SSO_REDIRECT_URI')}?access_token=${
userToken.token
}&expires_at=${expiresAt}&refresh_token=${newSession.refreshToken.token}`
)
}
} else {
// Existing user
const identity = await Identity.query()
.where('email', ssoUser.email)
.andWhere('provider', provider)
.first()

const lastSignInAt = DateTime.now()

if (!identity) {
await Identity.create({
userId: user.id,
provider,
identity_data: ssoUser.original,
email: ssoUser.email,
lastSignInAt,
})
}

const newSession = await Database.transaction(async (trx) => {
const session = await Session.create(
{
userId: user.id,
userAgent: headers['user-agent'],
ip: request.ips()[0],
},
{ client: trx }
)

const refreshToken = await RefreshToken.create(
{
userId: user.id,
sessionId: session.id,
token: cuid(),
revoked: false,
parent: null,
},
{ client: trx }
)

return {
session,
refreshToken,
}
})

if (identity) {
identity!.lastSignInAt = lastSignInAt
await identity?.save()
}

user.lastSignInAt = lastSignInAt
await user.save()

const userToken = this.jwt
.generate({ user_id: user.id, session_id: newSession.session.id })
.make()
const expiresAt = DateTime.now().plus({ days: 7 }).toUnixInteger()

return response.redirect(
`${Env.get('SSO_REDIRECT_URI')}?access_token=${
userToken.token
}&expires_at=${expiresAt}&refresh_token=${newSession.refreshToken.token}`
)
}
}
}
12 changes: 6 additions & 6 deletions app/Models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class User extends BaseModel {
public email: string | null

@column({ serializeAs: null })
public encryptedPassword: string
public encryptedPassword: string | null

@column.dateTime()
public emailConfirmedAt: DateTime
Expand All @@ -38,10 +38,10 @@ export default class User extends BaseModel {
public recoverySentAt: DateTime

@column({ serializeAs: null })
public emailChangeTokenNew: string
public emailChangeTokenNew: string | null

@column({ serializeAs: null })
public emailChange: string
public emailChange: string | null

@column.dateTime()
public emailChangeSentAt: DateTime
Expand All @@ -62,10 +62,10 @@ export default class User extends BaseModel {
public phoneConfirmedAt: DateTime

@column({ serializeAs: null })
public phoneChange: string
public phoneChange: string | null

@column({ serializeAs: null })
public phoneChangeToken: string
public phoneChangeToken: string | null

@column.dateTime()
public phoneChangeSentAt: DateTime
Expand All @@ -88,7 +88,7 @@ export default class User extends BaseModel {
@beforeSave()
public static async hashPassword(user: User) {
if (user.$dirty.encryptedPassword) {
user.encryptedPassword = await Hash.make(user.encryptedPassword)
user.encryptedPassword = await Hash.make(user.encryptedPassword!)
}
}

Expand Down
36 changes: 36 additions & 0 deletions config/ally.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Config source: https://git.io/JOdi5
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/

import Env from '@ioc:Adonis/Core/Env'
import { AllyConfig } from '@ioc:Adonis/Addons/Ally'

/*
|--------------------------------------------------------------------------
| Ally Config
|--------------------------------------------------------------------------
|
| The `AllyConfig` relies on the `SocialProviders` interface which is
| defined inside `contracts/ally.ts` file.
|
*/
const allyConfig: AllyConfig = {
/*
|--------------------------------------------------------------------------
| Google driver
|--------------------------------------------------------------------------
*/
google: {
driver: 'google',
clientId: Env.get('GOOGLE_CLIENT_ID'),
clientSecret: Env.get('GOOGLE_CLIENT_SECRET'),
callbackUrl: 'http://localhost:3333/auth/v1/sso/google/callback',
prompt: 'select_account',
scopes: ['userinfo.email', 'userinfo.profile'],
},
}

export default allyConfig
Loading

0 comments on commit 7535e9b

Please sign in to comment.