denoland / deno_kv_oauth

High-level OAuth 2.0 powered by Deno KV.

Home Page:https://jsr.io/@deno/kv-oauth

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

allow moving the authentication flow to a subdomain

slymax opened this issue · comments

commented

I'm not sure if this is supposed to work already, but I would like to have my authentication flow on auth.example.com and the main site on example.com. One issue I'm experiencing with this is that if the cookie gets set on the subdomain (by handleCallback), it gets blocked on the main domain, presumably because the Domain attribute is not set. Maybe making the domain attribute customizable would allow having the authentication flow on a subdomain?

https://web.dev/first-party-cookie-recipes/#first-party-cookie-recipe-for-sites-with-subdomains
https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_where_cookies_are_sent

commented

I think the prefix (__Secure instead of __Host) would have to be customizable for this as well. Maybe this would be a good reason to implement #20?

commented

So I've been tinkering a bit with this, and it looks like these steps are necessary to make this work:

  1. handleCallback must add the Domain attribute and use the __Secure prefix instead of __Host when setting the cookie.
  2. signIn, signOut, and getSessionId must be able to handle requests when a __Secure prefix is present instead of __Host.
  3. All responses sent to the client must have CORS enabled.

Here's a drop-in solution I've hacked together that works for my use case:

import { signIn, signOut, getSessionId, handleCallback } from "https://slymax.com/scripts/auth.js";

My proposal would be to introduce a handleCallbackWithSubdomain module that does [1], the functions in [2] become prefix-agnostic, and [3] is left to the developer before sending the response to the client.

Just curious, what is the use case for this? Sounds to me like the sub domain would be acting as a provider in the context of your main app

commented

the use case would be apps that follow the jamstack approach where the content served on example.com is static and possibly coming from another source (GitHub/Cloudflare Pages, Amazon S3, etc)

So what would your auth flow look like in this case?

commented

on example.com, there would be a link to auth.example.com/signin (or auth.example.com/signout) with a success_url (i.e. redirect) of example.com. From there, you could then fetch user data, access tokens, etc (e.g. from auth.example.com/user). This only works, however, if the cookie (set by handleCallback) is valid for auth.example.com and example.com.

@slymax, are you able to share the codebase for your use case?

3. All responses sent to the client must have CORS enabled.

I don't believe CORS is required. Changing the cookie name, adding the domain field and removing the path field should be sufficient.

This only works, however, if the cookie (set by handleCallback) is valid for auth.example.com and example.com.

In its current state, the cookie works if all auth happens in auth.example.com (within the same host). Excuse my ignorance, but could you please elaborate on why the cookie is also needed on example.com? Do you have an example? The use case makes sense, I just need to be able to justify the new requirement for this module.

commented

sure! I was thinking of doing something like this:

index.html served from a static file server on example.com:

<body>
    <a href="https://auth.example.com/login?success_url=https://example.com">Login</a><br>
    GitHub Access Token: <span id="access-token"></span>
    <script>
        const response = await fetch("https://auth.example.com/token", {
            credentials: "include"
        });
        const { token } = await response.json();
        document.getElementById("access-token").innerText = token;
    </script>
</body>

the server on auth.example.com:

import * as auth from "https://deno.land/x/deno_kv_oauth/mod.ts";

const config = auth.createGitHubOAuthConfig();

const routes = {
    login: async request => await auth.signIn(request, config),
    logout: async request => await auth.signOut(request, config),
    callback: async request => (await auth.handleCallback(request, config)).response,
    token: async request => {
        const sessionId = await auth.getSessionId(request);
        const token = await auth.getSessionAccessToken(config, sessionId);
        const response = Response.json({ token });
        const origin = request.headers.get("Origin");
        response.headers.set("Access-Control-Allow-Credentials", "true");
        response.headers.set("Access-Control-Allow-Origin", origin);
        return response;
    }
};

Deno.serve(request => {
    const { pathname } = new URL(request.url);
    return await routes[pathname.split("/")[1]](request);
});

I don't believe CORS is required. Changing the cookie name, adding the domain field and removing the path field should be sufficient.

yeah I think only the fetch requests from the client to the subdomain require CORS.


I've also been playing with this a little bit and it looks like this would also concern the cookie set by sign_in and, as mentioned above, signIn, signOut, and getSessionId would need to be able to handle cookies with a __Secure prefix, which isn't possible with the way cookie names are currently retrieved. Unfortunately, it's not as easy as adding an options interface to handleCallback.

I've created #245 which enables defining cookie options. Please give it a test and let me know how you go.

commented

yes, this works well. The removal of getSessionAccessToken in v0.8 makes use cases like this a bit more complicated but session cookies can now be set to be valid across subdomains 👍