I have a site hosted by Wix, and then a web app (built on sveltekit) hosted on vercel. I like the simplicity of having Wix handle things like memberships and payments, but my webapp is too complex to run on Wix.
I want to leverage the member login tooling on Wix to allow access to my web app. I have created an endpoint on Wix that retrieves the user id of the logged in user (if any) and then returns a signed JWT with the user id. If I make a request to this endpoint directly through my browser by entering the url in the address bar (similar to https://www.MyAmazingWebsite.com/_functions/SSO), I get the token as expected.
But within my web app (hosted, let's say, at https://MyAmazingWebApp.vercel.app), if I execute a fetch
to that endpoint I get a null
token, which is the expected behavior when no user is logged in. I assume this is because the session cookie for MyAmazingWebsite is not being sent along with my cross origin fetch
. I have tried setting the client options as follows after reviewing this SO question:
const response = await fetch(url, {
method: 'GET',
credentials: 'include'
})
And the server headers as follows:
let response = {
"headers": {
"Access-Control-Allow-Origin": "https://MyAmazingWebApp.vercel.app",
"Access-Control-Allow-Credentials": true,
// also tried "Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type, *",
"Content-Type": "application/json"
}
};
But still no luck.
Is it possible to do this? ("This" being using a session cookie to perform SSO in a cross origin way). I had assumed this is how SSO works, at least in some cases. (For example, sites that use Google for SSO don't ask me to log in to Google again if I am already authenticated with Google. But how would Google know who we are all talking about without a session cookie sent by my browser if I am not filling out a login form again?)
(Edit: as I think about it more, it seems the answer may be "it is not possible" and a redirect--not just an ajax request--will be necessary to establish my identity--via the session cookie in a same origin fashion--and verify I want to authorize my app to use my Wix site for SSO followed by generation of a session token for the web app. This will require a click by the user, but not re-entry of credentials. Notably, I see that if I redirect to the SSO endpoint rather than fetch
ing it, I get a JWT token for the logged in user as desired.)
Regardless of whether one CAN share cookies cross origin (which IIUC is actually possible), that is not the usual (or, likely, good) way to solve this problem.
Here are the basics of how I ultimately achieved SSO for my svelte-kit app using my Wix hosted site acting as the identity provider in a manner that is completely transparent and touch-free for the end user.
(Note: I spent WAY more time on this than it would have taken me to wire up industrial strength SSO using a provider like auth0. But, it was educational and I hope this efforts will shed some light on SSO mechanics for other interested parties. Needless to say I make no warranties as to the functionality or security of this code. Obviously, something like auth0 would be much more appropriate if you are trying to protect anything of value.)
The over all flow is:
Svelte-kit App redirects to Wix site passing desired session details as query term, then...
Wix site verifies that there is a user logged in, stores desired session details, redirects back to App with session token as query term, then...
App POSTs token back to get identity details and then sets session cookie, and redirects to originally requested protected URI.
Implementation
First, within my svelte-kit app, we redirect to the identity provider for protected routes if there is not a valid session cookie, keeping track of the requested URL and the authorization endpoint via query parameters. (Note that the HTTP response code is important--if you use 301, the permanent redirect will be cached by many browsers and then you will "never" be able to navigate back to your protected routes.)
host.config.js
const BASEURL = "https://MyAmazingWebApp.com/";
export const handle = async ({ event, resolve }) => {const session =
event.cookies.get('session');
const path = event.url.pathname;
if (!session) {
const regex = /^\/public/g;
const pub = path.match(regex);
if(!pub) {
throw redirect(307, process.env.HIGHFLASH_SSO_URI + "?" +
"requested_url=" + event.url +
"&auth_url=" + BASEURL + "public/auth/sso");
} else {
// public route ... proceed with request
return await resolve(event)
}
}
Now on my Wix hosted site, I create a GET
route to respond to authentication requests within a backend script.
http-functions.js
// Need this to actually get emails. Seems odd, but...
const collection_opts = {
"suppressAuth": true // !?
}
export async function get_hfpSSO(request) {
var type, id, session_id;
const requested_url = request.query.requested_url;
const auth_url = request.query.auth_url;
// retrieve id of logged in user, if any
try {
type = ident.identificationData.identities[0].person.type;
id = ident.identificationData.identities[0].person.id;
} catch(e) {
type = "VISITOR"
}
if(type != "VISITOR") {
/*
store the requested session details and the user's id as a
signed JWT in a DB (collection) on wix hosted site. Set
'session_id' to UUID of this row in DB so we can retrieve
these details after successful handshake
/*
} else {
session_id = "";
}
let res_options = {
status: 307,
headers: {
"Location" : auth_url + "?session_token=" + session_id
}
};
return response(res_options);
}
Back in my svelte-kit app, I define an endpoint to receive the authentication token and complete the handshake via POST. (I don't know if this is ultimately more secure then just using query terms, and I haven't studied the browser network traffic to verify). Note: After the handhsake is complete, I use an HTML meta refresh tag to go to the originally requested page instead of a redirect because I need to set a cookie. I cannot set a cookie with a redirect response.
The data returned by the POST includes a signed JWT that I verify with the corresponding public key stored locally.
$src/routes/public/sso/+server.js
import jwt from 'jsonwebtoken';
import fs from 'fs';
import { nanoid } from 'nanoid';
const BASEURL = "https://MyAmaxingWebApp.com/";
export async function GET({ request, url }) {
var session_token, destination;
const idp_session_token = url.searchParams.get('session_token');
if(idp_session_token) {
const response = await fetch("https://www.highflowpeds.com/_functions/hfpSSO",
{
method: 'POST',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({"session_token": idp_session_token})
})
const body = await response.json();
const session = {token: body.auth_token, destination: body.destination};
try {
// verify JWT signature of session.token, then...
session_token = nanoid();
destination = session.destination;
} catch(err) {
session_token = "";
destination = BASEURL + "public/auth/login";
}
} else {
session_token = "";
destination = BASEURL + "public/auth/login";
}
let res = new Response("<html><head><meta http-equiv='Refresh' content='0; url=" + destination + "'><head></html>");
res.headers.append("Content-Type", "text/html; charset=utf-8");
res.headers.append("Set-Cookie", "session=" + session_token + "; SameSite=Lax; HttpOnly; Path=/; Max-Age=86400");
return res;
}
On the Wix site end, this is the POST endpoint that provides the identity details as a signed JWT in response to a valid session token:
export async function post_hfpSSO(request) {
let response;
const d = await request.body.json();
const session_token = d.session_token;
if (session_token) {
// loading up the session details I saved to DB during the initial GET request.
const session = await wixData.query("SSO_sessions")
.eq("session_id", session_token)
.find(collection_opts);
response = {
body: {auth_token: session.items[0].auth_token, destination: session.items[0].destination},
headers: {
"Content-Type": "application/json"
}
};
} else {
response = {
body: {auth_token: "", destination: ""},
headers: {
"Content-Type": "application/json"
}
}
};
return ok(response);
}