Skip to content

Commit e4b9e24

Browse files
elf-pavlikmrkvontophcodesbourgeoa
committed
FedCM - a quick hack
Co-authored-by: mrkvon <mrkvon@protonmail.com> Co-authored-by: Christopher Mühl <toki@toph.so> Co-authored-by: bourgeoa <alain.bourgeois10@gmail.com>
1 parent 24489d9 commit e4b9e24

File tree

3 files changed

+113
-84
lines changed

3 files changed

+113
-84
lines changed

src/core/AuthorizationCodeGrant.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { requestDynamicClientRegistration } from "./DynamicClientRegistration";
33
import { ClientDetails, DynamicRegistrationClientDetails, IdentityProviderDetails, SessionInformation, TokenDetails } from "./SessionInformation";
44
import { SessionDatabase } from "./SessionDatabase";
55

6+
// @ts-ignore
7+
const buildRedirectUrl = (code, state, providerUrl) => {
8+
const base = window.location.href;
9+
return `${base}?code=${code}&state=${state}&iss=${encodeURIComponent(providerUrl)}`;
10+
};
11+
612
/**
713
* Login with the idp, using a provided `client_id` or dynamic client registration if none provided.
814
*
@@ -89,7 +95,30 @@ const redirectForLogin = async (idp: string, redirect_uri: string, client_detail
8995
`&state=${csrf_token}` +
9096
`&prompt=consent`; // this query parameter value MUST be present for CSS v7 to issue a refresh token ( // TODO open issue because prompting is the default behaviour but without this query param no refresh token is provided despite the "remember this client" box being checked)
9197

92-
window.location.href = redirect_to_idp;
98+
// do FedCM dance 💃🏻
99+
// do check first!
100+
const params = Object.fromEntries(new URL(redirect_to_idp).searchParams);
101+
const credential = await navigator.credentials.get({
102+
// @ts-ignore
103+
identity: {
104+
providers: [{
105+
configURL: 'any',
106+
clientId: params.client_id,
107+
registered: true,
108+
params: {
109+
code_challenge: params.code_challenge,
110+
code_challenge_method: params.code_challenge_method,
111+
state: params.state
112+
}
113+
}]
114+
}
115+
});
116+
console.log(credential)
117+
// XXX: we ♥️ trailing slash errors
118+
// @ts-ignore
119+
const fedCMissuer = new URL(credential.configURL).origin + '/'
120+
// @ts-ignore
121+
return buildRedirectUrl(credential.token, params.state, fedCMissuer)
93122
};
94123

95124
/**
@@ -119,8 +148,7 @@ const getPKCEcode = async () => {
119148
* URL contains authrization code, issuer (idp) and state (csrf token),
120149
* get an access token for the authrization code.
121150
*/
122-
const onIncomingRedirect = async (client_details?: ClientDetails, database?: SessionDatabase) => {
123-
const url = new URL(window.location.href);
151+
const onIncomingRedirect = async (url = new URL(window.location.href), client_details?: ClientDetails, database?: SessionDatabase) => {
124152
// authorization code
125153
const authorization_code = url.searchParams.get("code");
126154
// if no code, session remains unauthenticated at this point

src/core/Session.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ export class SessionCore extends EventTarget implements Session {
113113
}
114114

115115
async login(idp: string, redirect_uri: string) {
116-
await redirectForLogin(idp, redirect_uri, this.information.clientDetails)
116+
const fedCMFakeUrl = await redirectForLogin(idp, redirect_uri, this.information.clientDetails)
117+
await this.handleRedirectFromLogin(fedCMFakeUrl)
117118
}
118119

119120
/**
@@ -122,9 +123,9 @@ export class SessionCore extends EventTarget implements Session {
122123
* Upon success, it tries to persist information to refresh tokens in the session database.
123124
* If no database was provided, no information is persisted.
124125
*/
125-
async handleRedirectFromLogin() {
126+
async handleRedirectFromLogin(url?: string) {
126127
// Redirect after Authorization Code Grant // memory via sessionStorage
127-
const newSessionInfo = await onIncomingRedirect(this.information.clientDetails, this.database);
128+
const newSessionInfo = await onIncomingRedirect(url ? new URL(url) : undefined, this.information.clientDetails, this.database);
128129
// no session - we remain unauthenticated
129130
if (!newSessionInfo.tokenDetails) return;
130131
// we got a session

src/web/Session.ts

Lines changed: 78 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,98 +7,98 @@ import { SessionIDB } from './SessionDatabase';
77
// Any provided database via SessionOptions will be ignored.
88
// Database will be an IndexedDB.
99
export interface WebWorkerSessionOptions extends SessionOptions {
10-
workerUrl?: string | URL;
10+
workerUrl?: string | URL;
1111
}
1212

1313
/**
1414
* This Session provides background token refreshing using a Web Worker.
1515
*/
1616
export class WebWorkerSession extends SessionCore {
17-
private worker: SharedWorker;
17+
private worker: SharedWorker;
1818

19-
constructor(
20-
clientDetails?: DereferencableIdClientDetails | DynamicRegistrationClientDetails,
21-
sessionOptions?: WebWorkerSessionOptions
22-
) {
23-
const database = new SessionIDB();
24-
const options = { ...sessionOptions, database };
25-
super(clientDetails, options);
19+
constructor(
20+
clientDetails?: DereferencableIdClientDetails | DynamicRegistrationClientDetails,
21+
sessionOptions?: WebWorkerSessionOptions
22+
) {
23+
const database = new SessionIDB();
24+
const options = { ...sessionOptions, database };
25+
super(clientDetails, options);
2626

27-
// Allow consumer to provide worker URL, or use default
28-
const workerUrl = sessionOptions?.workerUrl ?? getWorkerUrl()
29-
this.worker = new SharedWorker(workerUrl, { type: 'module' });
30-
this.worker.port.onmessage = (event) => {
31-
this.handleWorkerMessage(event.data).catch(console.error);
32-
};
33-
window.addEventListener('beforeunload', () => {
34-
this.worker.port.postMessage({ type: RefreshMessageTypes.DISCONNECT });
35-
});
36-
}
37-
38-
private async handleWorkerMessage(data: any) {
39-
const { type, payload, error } = data;
40-
switch (type) {
41-
case RefreshMessageTypes.TOKEN_DETAILS:
42-
const wasActive = this.isActive;
43-
await this.setTokenDetails(payload.tokenDetails);
44-
if (wasActive !== this.isActive)
45-
this.dispatchStateChangeEvent();
46-
if (this.refreshPromise && this.resolveRefresh) {
47-
this.resolveRefresh();
48-
this.clearRefreshPromise();
49-
}
50-
break;
51-
case RefreshMessageTypes.ERROR_ON_REFRESH:
52-
if (this.isActive)
53-
this.dispatchExpirationWarningEvent();
54-
if (this.refreshPromise && this.rejectRefresh) {
55-
if (this.isActive) {
56-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
57-
} else {
58-
this.rejectRefresh(new Error("No session to restore"));
59-
}
60-
this.clearRefreshPromise();
61-
}
62-
break;
63-
case RefreshMessageTypes.EXPIRED:
64-
if (this.isActive) {
65-
this.dispatchExpirationEvent();
66-
await this.logout();
67-
}
68-
if (this.refreshPromise && this.rejectRefresh) {
69-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
70-
this.clearRefreshPromise();
71-
}
72-
break;
73-
}
27+
// Allow consumer to provide worker URL, or use default
28+
const workerUrl = sessionOptions?.workerUrl ?? getWorkerUrl()
29+
this.worker = new SharedWorker(workerUrl, { type: 'module' });
30+
this.worker.port.onmessage = (event) => {
31+
this.handleWorkerMessage(event.data).catch(console.error);
7432
};
33+
window.addEventListener('beforeunload', () => {
34+
this.worker.port.postMessage({ type: RefreshMessageTypes.DISCONNECT });
35+
});
36+
}
7537

76-
77-
async handleRedirectFromLogin() {
78-
await super.handleRedirectFromLogin();
79-
if (this.isActive) { // If login was successful, tell the worker to schedule refreshing
80-
this.worker.port.postMessage({
81-
type: RefreshMessageTypes.SCHEDULE,
82-
payload: { ...this.getTokenDetails(), expires_in: this.getExpiresIn() }
83-
});
38+
private async handleWorkerMessage(data: any) {
39+
const { type, payload, error } = data;
40+
switch (type) {
41+
case RefreshMessageTypes.TOKEN_DETAILS:
42+
const wasActive = this.isActive;
43+
await this.setTokenDetails(payload.tokenDetails);
44+
if (wasActive !== this.isActive)
45+
this.dispatchStateChangeEvent();
46+
if (this.refreshPromise && this.resolveRefresh) {
47+
this.resolveRefresh();
48+
this.clearRefreshPromise();
49+
}
50+
break;
51+
case RefreshMessageTypes.ERROR_ON_REFRESH:
52+
if (this.isActive)
53+
this.dispatchExpirationWarningEvent();
54+
if (this.refreshPromise && this.rejectRefresh) {
55+
if (this.isActive) {
56+
this.rejectRefresh(new Error(error || 'Token refresh failed'));
57+
} else {
58+
this.rejectRefresh(new Error("No session to restore"));
59+
}
60+
this.clearRefreshPromise();
61+
}
62+
break;
63+
case RefreshMessageTypes.EXPIRED:
64+
if (this.isActive) {
65+
this.dispatchExpirationEvent();
66+
await this.logout();
8467
}
68+
if (this.refreshPromise && this.rejectRefresh) {
69+
this.rejectRefresh(new Error(error || 'Token refresh failed'));
70+
this.clearRefreshPromise();
71+
}
72+
break;
8573
}
74+
};
8675

87-
async restore() {
88-
if (this.refreshPromise) {
89-
return this.refreshPromise;
90-
}
91-
this.refreshPromise = new Promise((resolve, reject) => {
92-
this.resolveRefresh = resolve;
93-
this.rejectRefresh = reject;
94-
});
95-
this.worker.port.postMessage({ type: RefreshMessageTypes.REFRESH });
96-
return this.refreshPromise;
76+
77+
async handleRedirectFromLogin(url: string) {
78+
await super.handleRedirectFromLogin(url);
79+
if (this.isActive) { // If login was successful, tell the worker to schedule refreshing
80+
this.worker.port.postMessage({
81+
type: RefreshMessageTypes.SCHEDULE,
82+
payload: { ...this.getTokenDetails(), expires_in: this.getExpiresIn() }
83+
});
9784
}
85+
}
9886

99-
async logout() {
100-
this.worker.port.postMessage({ type: RefreshMessageTypes.STOP });
101-
await super.logout();
87+
async restore() {
88+
if (this.refreshPromise) {
89+
return this.refreshPromise;
10290
}
91+
this.refreshPromise = new Promise((resolve, reject) => {
92+
this.resolveRefresh = resolve;
93+
this.rejectRefresh = reject;
94+
});
95+
this.worker.port.postMessage({ type: RefreshMessageTypes.REFRESH });
96+
return this.refreshPromise;
97+
}
10398

104-
}
99+
async logout() {
100+
this.worker.port.postMessage({ type: RefreshMessageTypes.STOP });
101+
await super.logout();
102+
}
103+
104+
}

0 commit comments

Comments
 (0)