I'm trying to authenticate users via magic link with vanilla JS, Supabase, and Netlify functions — keeping all the authentication on the server side and keys out of the browser.
I can successfully send a magic link to a user, which redirects them to a Netlify function. However, when the user opens that link, I'm not sure how to get the code
for supabase.auth.exchangeCodeForSession(code)
to actually authenticate the session/user in the Netlify function. You can see my current code for that function here:
require('dotenv').config();
const querystring = require('querystring');
const { createClient } = require('@supabase/supabase-js');
const { access } = require('fs');
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_API_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
exports.handler = async (url, event, headers, request, cookies, req, res, context, body) => {
console.log('url')
console.log(url)
console.log('context')
console.log(context)
console.log('body')
console.log(body)
console.log('event')
console.log(event)
console.log('headers')
console.log(headers)
console.log('cookies')
console.log(cookies)
console.log(request);
console.log(req)
const code = url.headers['cookie'];
console.log(code);
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
if (error) {
return {
statusCode: 500,
body: JSON.stringify(error),
};
} else {
console.log(JSON.stringify(data))
return {
statusCode: 200,
body: (JSON.stringify(data))
};
}
}
This is currently resulting in the error:
AuthApiError - unsupported_grant_type
/Users/sam/coop-media/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:41:20
processTicksAndRejections (node:internal/process/task_queues:96:5)
I would add here the code for the function that is generating the magic link (which works as expected, seemingly):
require('dotenv').config();
const querystring = require('querystring');
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_API_KEY;
const supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
flowType: 'pkce',
},
})
exports.handler = async (event) => {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
console.log('test');
const form = querystring.parse(event.body);
console.log(form.email);
const { user, session, error } = await supabase.auth.signInWithOtp({
email: form.email,
options: {
emailRedirectTo: 'http://localhost:8888/getSessionWithOtp',
},
})
if (error) {
return {
statusCode: 500,
body: JSON.stringify(error),
};
} else {
console.log(JSON.stringify(user))
return {
statusCode: 200,
body: (JSON.stringify(user), session)
};
}
}
As well as the Netlify.toml file which is handling the redirect:
[[redirects]]
from = "/getSessionWithOtp/*"
to = "/.netlify/functions/getSessionWithOtp/:splat"
status = 200
Update - Solution
Netlify redirects erase all data after #
— and the magic link urls from Supabase contain a #, after which all of the tokens are included.
I solved this by first redirecting the magic link to a client side page, then taking the refresh token from the magic link url in the browser and submitting it as a query parameter in an XHR request to a Netlify function page, and authenticating by getting the refresh token from the query parameter in the Netlify function page and sending it to Supabase with this code (after creating the Supabase client in the Netlify function page):
const { data, error } = await supabase.auth.refreshSession({ refresh_token })
const { session, user } = data
I also learned that Netlify redirects in general erase query parameters, and the redirect or Netlify.toml file must be adjusted to include query parameters that should be included on the redirect (documentation here: https://docs.netlify.com/routing/redirects/redirect-options/, as well as in various Netlify forum posts)