-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WS-2: Implement mobile sign in (#619)
Closes #613
- Loading branch information
Showing
21 changed files
with
639 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { ObjectId } from 'mongodb' | ||
|
||
export type DeviceAuthorization = { | ||
_id: string | ||
user: ObjectId | ||
signature: string | ||
expiresAt: Date | ||
} |
46 changes: 46 additions & 0 deletions
46
src/backend/auth/authenticateDeviceAuthorizationSignature.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { collections } from '$constants/mongo' | ||
import { TRPCError } from '@trpc/server' | ||
import { ObjectId } from 'mongodb' | ||
import { generateMessageHash } from '../signatures/generateSignature' | ||
import { verifySignature } from '../signatures/verifySignature' | ||
import { finalizeAuthentication } from './finalizeAuthentication' | ||
|
||
export async function authenticateDeviceAuthorizationSignature( | ||
deviceId: string, | ||
signature: string | ||
) { | ||
const result = await verifySignature(signature) | ||
if (!result.verified) { | ||
throw new TRPCError({ | ||
code: 'UNAUTHORIZED', | ||
message: 'Signature is not verified', | ||
}) | ||
} | ||
|
||
const expectedMessage = `mobileAuthorize:${deviceId}` | ||
const expectedMessageHash = generateMessageHash(expectedMessage) | ||
if (result.messageHash !== expectedMessageHash) { | ||
throw new TRPCError({ | ||
code: 'UNAUTHORIZED', | ||
message: 'Signature is not for mobile authorization', | ||
}) | ||
} | ||
|
||
if (Date.parse(result.timestamp) < Date.now() - 15 * 60e3) { | ||
throw new TRPCError({ | ||
code: 'UNAUTHORIZED', | ||
message: 'Signature is expired', | ||
}) | ||
} | ||
|
||
const userId = result.userId | ||
const user = await collections.users.findOne({ _id: new ObjectId(userId) }) | ||
if (!user) { | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'User not found in database. This should not happen.', | ||
}) | ||
} | ||
|
||
return finalizeAuthentication(user.uid) | ||
} |
20 changes: 20 additions & 0 deletions
20
src/backend/deviceAuthorizations/getDeviceAuthorization.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { collections } from '$constants/mongo' | ||
import { sha256 } from '$functions/sha256' | ||
|
||
export async function getDeviceAuthorization( | ||
deviceIdBasis: string | ||
): Promise<{ found: false } | { found: true; signature: string }> { | ||
const deviceAuthorization = await collections.deviceAuthorizations.findOne({ | ||
_id: await sha256(deviceIdBasis), | ||
expiresAt: { $gt: new Date() }, | ||
}) | ||
if (!deviceAuthorization) { | ||
return { | ||
found: false, | ||
} | ||
} | ||
return { | ||
found: true, | ||
signature: deviceAuthorization.signature, | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
src/backend/deviceAuthorizations/saveDeviceAuthorization.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { collections } from '$constants/mongo' | ||
import type { AuthenticatedUser } from '$types/AuthenticatedUser' | ||
import { TRPCError } from '@trpc/server' | ||
|
||
export async function saveDeviceAuthorization( | ||
user: AuthenticatedUser | null, | ||
deviceId: string, | ||
signature: string | ||
) { | ||
if (!user) { | ||
throw new TRPCError({ | ||
code: 'UNAUTHORIZED', | ||
message: 'User is not authenticated', | ||
}) | ||
} | ||
|
||
const userDoc = await collections.users.findOne({ uid: user.uid }) | ||
if (!userDoc) { | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'User not found in database. This should not happen.', | ||
}) | ||
} | ||
|
||
const existingDeviceAuthorization = | ||
await collections.deviceAuthorizations.findOne({ | ||
_id: deviceId, | ||
}) | ||
if (existingDeviceAuthorization) { | ||
return { expiresAt: existingDeviceAuthorization.expiresAt } | ||
} | ||
|
||
const expiresAt = new Date(Date.now() + 10 * 60e3) | ||
await collections.deviceAuthorizations.insertOne({ | ||
_id: deviceId, | ||
user: userDoc._id, | ||
signature, | ||
expiresAt, | ||
}) | ||
|
||
return { expiresAt } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import type { AuthenticatedUser } from '$types/AuthenticatedUser' | ||
import { createHash, createHmac } from 'crypto' | ||
import { ObjectId } from 'mongodb' | ||
|
||
export const generateMessageHash = (msg: string) => { | ||
return createHash('sha256').update(msg).digest('hex').slice(0, 24) | ||
} | ||
|
||
export const generateSignature = (user: AuthenticatedUser, msg: string) => { | ||
const key = process.env.SIGN_KEY_01 | ||
if (!key) { | ||
throw new Error('SIGN_KEY_01 is not set') | ||
} | ||
const messageHash = generateMessageHash(msg) | ||
const prefix = [ | ||
'grtn', | ||
'v1', | ||
messageHash, | ||
user.sub, | ||
new ObjectId().toString(), | ||
].join('_') | ||
const signature = createHmac('sha256', key) | ||
.update(prefix) | ||
.digest('hex') | ||
.slice(0, 24) | ||
return { signature: `${prefix}_${signature}`, messageHash } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { createHmac } from 'crypto' | ||
import { ObjectId } from 'mongodb' | ||
|
||
export const verifySignature = async ( | ||
signature: string | ||
): Promise< | ||
| { | ||
verified: true | ||
messageHash: string | ||
userId: string | ||
timestamp: string | ||
nonce: string | ||
} | ||
| { verified: false; error: string } | ||
> => { | ||
const parts = signature.split('_') | ||
const fail = (message: string) => ({ | ||
verified: false as const, | ||
error: message, | ||
}) | ||
if (parts[0] !== 'grtn') { | ||
return fail('Invalid signature format') | ||
} | ||
switch (parts[1]) { | ||
case 'v1': { | ||
const key = process.env.SIGN_KEY_01 | ||
if (!key) { | ||
throw new Error('SIGN_KEY_01 is not set') | ||
} | ||
const [, , messageHash, userId, nonce, signatureHash] = parts | ||
const prefix = parts.slice(0, 5).join('_') | ||
const signature = createHmac('sha256', key) | ||
.update(prefix) | ||
.digest('hex') | ||
.slice(0, 24) | ||
if (signature !== signatureHash) { | ||
return fail('Signature mismatch') | ||
} | ||
return { | ||
verified: true, | ||
messageHash, | ||
userId, | ||
timestamp: new ObjectId(nonce).getTimestamp().toISOString(), | ||
nonce: nonce, | ||
} | ||
} | ||
default: { | ||
return fail('Invalid signature version') | ||
} | ||
} | ||
} |
Oops, something went wrong.