Skip to content

Commit

Permalink
fix(auth): fix bug where user can login even though blocked
Browse files Browse the repository at this point in the history
When refresh token compromise is detected, We block the user, but the user can still login. This PR
fixes that by adding a check during the login flow to make sure the user cannot login when blocked
  • Loading branch information
Frantz Kati committed Nov 28, 2020
1 parent 158bdf3 commit 4a78925
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 20 deletions.
1 change: 1 addition & 0 deletions examples/blog/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = tensei()
.verifyEmails()
.teams()
.apiPath('auth')
.noCookies()
.rolesAndPermissions()
.social('github', {
key: process.env.GITHUB_KEY,
Expand Down
39 changes: 19 additions & 20 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class Auth {
.creationRules('required')
.onlyOnForms()
.hideOnUpdate(),
dateTime('Blocked At').nullable(),
...socialFields,
...(this.config.rolesAndPermissions
? [belongsToMany(this.config.roleResource)]
Expand Down Expand Up @@ -469,7 +470,7 @@ class Auth {
await this.setAuthUserForPublicRoutes(
context
)
await this.ensureAuthUserIsNotCompromised(
await this.ensureAuthUserIsNotBlocked(
context
)
await this.authorizeResolver(context, query)
Expand Down Expand Up @@ -521,9 +522,7 @@ class Auth {
ctx
),
async (parent, args, ctx, info) =>
this.ensureAuthUserIsNotCompromised(
ctx
)
this.ensureAuthUserIsNotBlocked(ctx)
])
}
})
Expand Down Expand Up @@ -683,7 +682,7 @@ class Auth {
return next()
},
async (request, response, next) => {
await this.ensureAuthUserIsNotCompromised(
await this.ensureAuthUserIsNotBlocked(
request as any
)

Expand Down Expand Up @@ -1313,7 +1312,14 @@ class Auth {
compromised_at: Dayjs().format()
})

await ctx.manager.persistAndFlush(token)
ctx.manager.assign(token[userField], {
blocked_at: Dayjs().format()
})

ctx.manager.persist(token)
ctx.manager.persist(token[userField])

await ctx.manager.flush()

throw ctx.authenticationError('Invalid refresh token.')
}
Expand Down Expand Up @@ -1664,6 +1670,10 @@ class Auth {
throw ctx.authenticationError('Invalid credentials.')
}

if (user.blocked_at) {
throw ctx.forbiddenError('Your account is temporarily disabled.')
}

if (!Bcrypt.compareSync(password, user.password)) {
throw ctx.authenticationError('Invalid credentials.')
}
Expand Down Expand Up @@ -1740,24 +1750,13 @@ class Auth {
}
}

private ensureAuthUserIsNotCompromised = async (ctx: ApiContext) => {
private ensureAuthUserIsNotBlocked = async (ctx: ApiContext) => {
if (!ctx.user || (ctx.user && ctx.user.public)) {
return
}

const compromisedTokensCount = await ctx.manager.count(
this.resources.token.data.pascalCaseName,
{
[this.resources.user.data.snakeCaseName]: ctx.user.id,
type: TokenTypes.REFRESH,
compromised_at: {
$ne: null
}
}
)

if (compromisedTokensCount) {
throw ctx.forbiddenError('Credentials have been compromised.')
if (ctx.user.blocked_at) {
throw ctx.forbiddenError('Your account is temporarily disabled.')
}
}

Expand Down
116 changes: 116 additions & 0 deletions packages/tests/packages/auth/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,119 @@ test('access tokens and refresh tokens are generated correctly', async done => {

// @ts-ignore
}, 10000)

test('if a refresh token is used twice (compromised), the user is automatically blocked', async () => {
const {
ctx: {
orm: { em }
},
app
} = await setup([
auth()
.user('Customer')
.noCookies()
.plugin(),
graphql().plugin()
])

const client = Supertest(app)

const user = em.create('Customer', fakeUser())

await em.persistAndFlush(user)

const login_response = await client.post(`/graphql`).send({
query: gql`
mutation login_customer($email: String!, $password: String!) {
login_customer(object: { email: $email, password: $password }) {
access_token
refresh_token
customer {
id
email
}
}
}
`,
variables: {
password: 'password',
email: user.email
}
})

const { refresh_token } = login_response.body.data.login_customer

const refresh_token_response = await client.post(`/graphql`).send({
query: gql`
mutation refresh_token($refresh_token: String!) {
refresh_token(object: { refresh_token: $refresh_token }) {
access_token
refresh_token
customer {
email
}
}
}
`,
variables: {
refresh_token
}
})

expect(refresh_token_response.status).toBe(200)
expect(refresh_token_response.body.data.refresh_token).toEqual({
access_token: expect.any(String),
refresh_token: expect.any(String),
customer: {
email: expect.any(String)
}
})
const compromised_refresh_token_response = await client
.post(`/graphql`)
.send({
query: gql`
mutation refresh_token($refresh_token: String!) {
refresh_token(object: { refresh_token: $refresh_token }) {
access_token
refresh_token
customer {
id
email
}
}
}
`,
variables: {
refresh_token
}
})

expect(compromised_refresh_token_response.status).toBe(200)
expect(compromised_refresh_token_response.body.errors[0].message).toBe(
'Invalid refresh token.'
)

const login_response_after_blocked = await client.post(`/graphql`).send({
query: gql`
mutation login_customer($email: String!, $password: String!) {
login_customer(object: { email: $email, password: $password }) {
access_token
refresh_token
customer {
id
email
}
}
}
`,
variables: {
password: 'password',
email: user.email
}
})

expect(login_response_after_blocked.status).toBe(200)
expect(login_response_after_blocked.body.errors[0].message).toBe(
'Your account is temporarily disabled.'
)
})

0 comments on commit 4a78925

Please sign in to comment.