Skip to content

Commit

Permalink
Add JWT re-use + logout, add page refresh support with existing JWT
Browse files Browse the repository at this point in the history
  • Loading branch information
WietseWind committed Nov 7, 2022
1 parent 2639b2a commit fedb375
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 11 deletions.
11 changes: 10 additions & 1 deletion sample/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ <h2>Hello, world!</h2>
<button id="signinbtn" onclick="go()" class="btn btn-outline-primary">...</button>
<div class="alert alert-success" id="signedin">Signed in :)</div>
<pre style="text-align: left; display: none;" id="results">...</pre>
<button style="display: none; float: right;" id="logout" onclick="go_logout()" class="btn btn-danger">Logout</button>
<button style="display: none;" id="trypayload" onclick="go_payload()" class="btn btn-primary">Now try a Sign Request</button>
</div>
</div>
Expand Down Expand Up @@ -74,6 +75,8 @@ <h2>Hello, world!</h2>
document.getElementById('signedin').style.display = 'none'
document.getElementById('error').style.display = 'none'
document.getElementById('trypayload').style.display = 'none'
document.getElementById('logout').style.display = 'none'
document.getElementById('results').style.display = 'none'
}

// Start in default UI state
Expand Down Expand Up @@ -102,6 +105,7 @@ <h2>Hello, world!</h2>
resultspre.style.display = 'block'
resultspre.innerText = JSON.stringify(authorized.me, null, 2)
document.getElementById('trypayload').style.display = 'block'
document.getElementById('logout').style.display = 'block'

sdk.ping().then(pong => console.log({pong}))
})
Expand All @@ -115,14 +119,19 @@ <h2>Hello, world!</h2>
})
}

function go_logout() {
auth.logout()
reset()
signinbtn.style.display = 'inline-block'
}

function go_payload() {
/**
* xumm-oauth2-pkce package returns `sdk` property,
* allowing access to the Xumm SDK (`xumm-sdk`) package.
* Xumm SDK methods, docs:
* https://www.npmjs.com/package/xumm-sdk
**/

var payload = {
txjson: {
TransactionType: 'Payment',
Expand Down
151 changes: 141 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const log = Debug("xummpkce");

log("Xumm OAuth2 PKCE Authorization Code Flow lib.");

interface XummPkceOptions {
redirectUrl: string;
rememberJwt: boolean;
storage: Storage;
}
interface ResolvedFlow {
sdk: XummSdkJwt;
jwt: string;
Expand Down Expand Up @@ -51,34 +56,102 @@ export declare interface XummPkce {

export class XummPkce extends EventEmitter {
private pkce: PKCE;
private options: XummPkceOptions;
private popup: Window | null = null;

private jwt?: string;

private resolved = false;
private resolvePromise?: (result: ResolvedFlow) => void;
private rejectPromise?: (error: Error) => void;
private promise?: Promise<ResolvedFlow>;
private autoResolvedFlow?: ResolvedFlow;

private mobileRedirectFlow: boolean = false;
private urlParams?: URLSearchParams;

constructor(xummApiKey: string, redirectUrl?: string) {
constructor(
xummApiKey: string,
optionsOrRedirectUrl?: string | XummPkceOptions
) {
super();

this.options = {
redirectUrl: document.location.href,
rememberJwt: true,
storage: localStorage,
};

/**
* Apply options
*/
if (typeof optionsOrRedirectUrl === "string") {
this.options.redirectUrl = optionsOrRedirectUrl;
} else if (
typeof optionsOrRedirectUrl === "object" &&
optionsOrRedirectUrl
) {
if (typeof this.options.redirectUrl === "string") {
this.options.redirectUrl = this.options.redirectUrl;
}
if (typeof this.options.rememberJwt === "boolean") {
this.options.rememberJwt = this.options.rememberJwt;
}
if (typeof this.options.storage === "object") {
this.options.storage = this.options.storage;
}
}

/**
* Construct
*/
this.pkce = new PKCE({
client_id: xummApiKey,
redirect_uri: redirectUrl || document.location.href,
redirect_uri: this.options.redirectUrl,
authorization_endpoint: "https://oauth2.xumm.app/auth",
token_endpoint: "https://oauth2.xumm.app/token",
requested_scopes: "XummPkce",
storage: localStorage,
storage: this.options.storage,
});

/**
* Check if there is already a valid JWT to be used
*/
if (this.options.rememberJwt) {
log("Remember JWT");
try {
const existingJwt = JSON.parse(
this.options.storage?.getItem("XummPkceJwt") || "{}"
);

if (existingJwt?.jwt && typeof existingJwt.jwt === "string") {
const sdk = new XummSdkJwt(existingJwt.jwt);
sdk.ping().then((pong) => {
/**
* Pretend mobile so no window.open is triggered
*/
if (pong?.jwtData?.sub) {
// Yay, user still signed in, JWT still valid!
this.autoResolvedFlow = Object.assign(existingJwt, { sdk });
this.emit("result");
} else {
this.logout();
}
});
}
} catch (e) {
// Do nothing
}
}

window.addEventListener(
"message",
(event) => {
log("Received Event from ", event.origin);
if (String(event?.data || '').slice(0, 1) === "{" && String(event?.data || '').slice(-1) === "}") {
if (
String(event?.data || "").slice(0, 1) === "{" &&
String(event?.data || "").slice(-1) === "}"
) {
log("Got PostMessage with JSON");
if (
event.origin === "https://xumm.app" ||
Expand All @@ -103,6 +176,14 @@ export class XummPkce extends EventEmitter {
"Payload resolved, mostmessage containing options containing redirect URL: ",
postMessage
);

/**
* Beat the 750ms timing for the window close as the exchange
* may still take a whil (async HTTP call). We don't know YET
* if we resolved successfully but we sure did resolve.
*/
this.resolved = true;

this.pkce
.exchangeForAccessToken(postMessage.options.full_redirect_uri)
.then((resp) => {
Expand All @@ -125,6 +206,18 @@ export class XummPkce extends EventEmitter {
.then((r) => r.json())
.then((me) => {
if (this.resolvePromise) {
if (this.options.rememberJwt) {
log("Remembering JWT");
try {
this.options.storage?.setItem(
"XummPkceJwt",
JSON.stringify({ jwt: resp.access_token, me })
);
} catch (e) {
log("Could not persist JWT to local storage", e);
}
}

this.resolvePromise({
jwt: resp.access_token,
sdk: new XummSdkJwt(resp.access_token),
Expand All @@ -151,8 +244,18 @@ export class XummPkce extends EventEmitter {
)
);
}
} else if (
postMessage?.source === "xumm_sign_request_popup_closed"
) {
log("Popup closed, wait 750ms");
// Wait, maybe the real reason comes in later (e.g. explicitly rejected)
setTimeout(() => {
if (!this.resolved && this.rejectPromise) {
this.rejectPromise(new Error("Sign In window closed"));
}
}, 750);
} else {
log("Unexpected message, skipping");
log("Unexpected message, skipping", postMessage?.source);
}
} catch (e: unknown) {
log("Error parsing message", (e as Error)?.message || e);
Expand Down Expand Up @@ -217,7 +320,7 @@ export class XummPkce extends EventEmitter {
}

public async authorize() {
if (!this.mobileRedirectFlow) {
if (!this.mobileRedirectFlow && !this.autoResolvedFlow) {
const url = this.authorizeUrl();
const popup = window.open(
url,
Expand All @@ -227,17 +330,45 @@ export class XummPkce extends EventEmitter {
);

this.popup = popup;

log("Popup opened...", url);
}

this.promise = new Promise((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
});
this.resolved = false;

if (this.autoResolvedFlow) {
this.resolved = true;
this.promise = Promise.resolve(this.autoResolvedFlow);
this.rejectPromise = this.resolvePromise = () => {};
log("Auto resolved");
} else {
this.promise = new Promise((resolve, reject) => {
this.resolvePromise = (_) => {
this.resolved = true;
log("Xumm Sign in RESOLVED");
return resolve(_);
};
this.rejectPromise = (_) => {
this.resolved = true;
log("Xumm Sign in REJECTED");
return reject(_);
};
});
}

return this.promise;
}

public logout() {
try {
this.autoResolvedFlow = undefined;
this.options.storage?.removeItem("XummPkceJwt");
} catch (e) {
// Nothing to do
}
return;
}

public getPopup() {
return this?.popup;
}
Expand Down

0 comments on commit fedb375

Please sign in to comment.