Skip to content

Commit

Permalink
Support magic links for OTP emails
Browse files Browse the repository at this point in the history
  • Loading branch information
dfahlander committed Jan 31, 2024
1 parent 582006e commit 12d552e
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 24 deletions.
15 changes: 10 additions & 5 deletions addons/dexie-cloud/src/DexieCloudAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import { BehaviorSubject, Observable } from 'rxjs';

/** The API of db.cloud, where `db` is an instance of Dexie with dexie-cloud-addon active.
*/

export interface LoginHints {
email?: string;
userId?: string;
grant_type?: 'demo' | 'otp';
otpId?: string;
otp?: string;
}

export interface DexieCloudAPI {
// Version of dexie-cloud-addon
version: string;
Expand Down Expand Up @@ -67,11 +76,7 @@ export interface DexieCloudAPI {
* @param userId Optional userId to authenticate
* @param grant_type requested grant type
*/
login(hint?: {
email?: string;
userId?: string;
grant_type?: 'demo' | 'otp';
}): Promise<void>;
login(hint?: LoginHints): Promise<void>;

logout(options?: {force?: boolean}): Promise<void>;

Expand Down
7 changes: 4 additions & 3 deletions addons/dexie-cloud/src/authentication/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import {
import { TokenErrorResponseError } from './TokenErrorResponseError';
import { alertUser, interactWithUser } from './interactWithUser';
import { InvalidLicenseError } from '../InvalidLicenseError';
import { LoginHints } from '../DexieCloudAPI';

export type FetchTokenCallback = (tokenParams: {
public_key: string;
hints?: { userId?: string; email?: string; grant_type?: string };
hints?: LoginHints;
}) => Promise<TokenFinalResponse | TokenErrorResponse>;

export async function loadAccessToken(
Expand Down Expand Up @@ -63,7 +64,7 @@ export async function authenticate(
context: UserLogin,
fetchToken: FetchTokenCallback,
userInteraction: BehaviorSubject<DXCUserInteraction | undefined>,
hints?: { userId?: string; email?: string; grant_type?: string }
hints?: LoginHints
): Promise<UserLogin> {
if (
context.accessToken &&
Expand Down Expand Up @@ -145,7 +146,7 @@ async function userAuthenticate(
context: UserLogin,
fetchToken: FetchTokenCallback,
userInteraction: BehaviorSubject<DXCUserInteraction | undefined>,
hints?: { userId?: string; email?: string; grant_type?: string }
hints?: LoginHints
) {
if (!crypto.subtle) {
if (typeof location !== 'undefined' && location.protocol === 'http:') {
Expand Down
3 changes: 2 additions & 1 deletion addons/dexie-cloud/src/authentication/login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DexieCloudDB } from '../db/DexieCloudDB';
import { LoginHints } from '../DexieCloudAPI';
import { triggerSync } from '../sync/triggerSync';
import { authenticate, loadAccessToken } from './authenticate';
import { AuthPersistedContext } from './AuthPersistedContext';
Expand All @@ -9,7 +10,7 @@ import { UNAUTHORIZED_USER } from './UNAUTHORIZED_USER';

export async function login(
db: DexieCloudDB,
hints?: { email?: string; userId?: string; grant_type?: string }
hints?: LoginHints
) {
const currentUser = await db.getCurrentUser();
const origUserId = currentUser.userId;
Expand Down
35 changes: 26 additions & 9 deletions addons/dexie-cloud/src/authentication/otpFetchTokenCallback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {
DemoTokenRequest,
OTPTokenRequest1,
OTPTokenRequest2,
TokenErrorResponse,
TokenFinalResponse,
TokenRequest,
Expand All @@ -25,8 +28,20 @@ export function otpFetchTokenCallback(db: DexieCloudDB): FetchTokenCallback {
demo_user,
grant_type: 'demo',
scopes: ['ACCESS_DB'],
public_key
} satisfies DemoTokenRequest;
} else if (hints?.otpId && hints.otp) {
// User provided OTP ID and OTP code. This means that the OTP email
// has already gone out and the user may have clicked a magic link
// in the email with otp and otpId in query and the app has picked
// up those values and passed them to db.cloud.login().
tokenRequest = {
grant_type: 'otp',
otp_id: hints.otpId,
otp: hints.otp,
scopes: ['ACCESS_DB'],
public_key,
};
} satisfies OTPTokenRequest2;
} else {
const email = await promptForEmail(
userInteraction,
Expand All @@ -37,8 +52,7 @@ export function otpFetchTokenCallback(db: DexieCloudDB): FetchTokenCallback {
email,
grant_type: 'otp',
scopes: ['ACCESS_DB'],
public_key,
};
} satisfies OTPTokenRequest1;
}
const res1 = await fetch(`${url}/token`, {
body: JSON.stringify(tokenRequest),
Expand All @@ -60,29 +74,32 @@ export function otpFetchTokenCallback(db: DexieCloudDB): FetchTokenCallback {
// Demo user request can get a "tokens" response right away
// Error can also be returned right away.
return response;
} else if (tokenRequest.grant_type === 'otp') {
} else if (tokenRequest.grant_type === 'otp' && 'email' in tokenRequest) {
if (response.type !== 'otp-sent')
throw new Error(`Unexpected response from ${url}/token`);
const otp = await promptForOTP(userInteraction, tokenRequest.email);
tokenRequest.otp = otp || '';
tokenRequest.otp_id = response.otp_id;
const tokenRequest2 = {
...tokenRequest,
otp: otp || '',
otp_id: response.otp_id,
} satisfies OTPTokenRequest2;

let res2 = await fetch(`${url}/token`, {
body: JSON.stringify(tokenRequest),
body: JSON.stringify(tokenRequest2),
method: 'post',
headers: { 'Content-Type': 'application/json' },
mode: 'cors',
});
while (res2.status === 401) {
const errorText = await res2.text();
tokenRequest.otp = await promptForOTP(userInteraction, tokenRequest.email, {
tokenRequest2.otp = await promptForOTP(userInteraction, tokenRequest.email, {
type: 'error',
messageCode: 'INVALID_OTP',
message: errorText,
messageParams: {}
});
res2 = await fetch(`${url}/token`, {
body: JSON.stringify(tokenRequest),
body: JSON.stringify(tokenRequest2),
method: 'post',
headers: { 'Content-Type': 'application/json' },
mode: 'cors',
Expand Down
2 changes: 1 addition & 1 deletion libs/dexie-cloud-common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dexie-cloud-common",
"version": "1.0.31",
"version": "1.0.32",
"description": "Library for shared code between dexie-cloud-addon, dexie-cloud (CLI) and dexie-cloud-server",
"type": "module",
"module": "dist/index.js",
Expand Down
17 changes: 12 additions & 5 deletions libs/dexie-cloud-common/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
export type TokenRequest =
| OTPTokenRequest
| OTPTokenRequest1
| OTPTokenRequest2
| ClientCredentialsTokenRequest
| RefreshTokenRequest
| DemoTokenRequest;

export interface OTPTokenRequest {
export type OTPTokenRequest = OTPTokenRequest1 | OTPTokenRequest2;
export interface OTPTokenRequest1 {
grant_type: 'otp';
public_key?: string; // If a refresh token is requested. Clients own the keypair and sign refresh_token requests using it.
email: string;
scopes: string[]; // TODO use CLIENT_SCOPE type.
otp_id?: string;
otp?: string;
}

export interface OTPTokenRequest2 {
grant_type: 'otp';
public_key?: string;
scopes: string[]; // TODO use CLIENT_SCOPE type.
otp_id: string;
otp: string;
}

export interface ClientCredentialsTokenRequest {
Expand Down

0 comments on commit 12d552e

Please sign in to comment.