atinux / nuxt-auth-utils

Add Authentication to Nuxt applications with secured & sealed cookies sessions.
MIT License
998 stars 92 forks source link

[Question] Expose/access the tokens #34

Closed victorlmneves closed 3 weeks ago

victorlmneves commented 11 months ago

Hi First, thanks for the great work

Currently, I'm working on a POC to migrate our Vue 3 SSR app to Nuxt 3 and I'm trying a few libs to replace the express-openid-connect that we are using to manage the authentication.

The question is that on each request to our APIs I need to send the token and also check if the token is still valid. From what I see from the example, this is only done during the authentication process.

Is it possible to access/expose the tokens that are returned at onSuccess from https://github.com/Atinux/nuxt-auth-utils/blob/10398a61986df2919df38e9d37cfbb7ae991dd32/src/runtime/server/lib/oauth/auth0.ts#L87-L126

Thanks

PS: Maybe it would be great to have a "Discussions" page for this kind of questions

RihanArfan commented 10 months ago

Had the same query and figured it out. It's exposed where you register the OAuth event handler.

// /server/routes/auth/sso.ts
export default oauth.microsoftEventHandler({
  async onSuccess(event, { user, tokens }) { // <-- tokens exposed here
    console.log("Tokens", tokens);

    await setUserSession(event, {
      user: {
        microsoft: user,
      },
      loggedInAt: Date.now(),
    });

    return sendRedirect(event, "/");
  },
});

Be aware that storing the token within setUserSession() will expose it to the client at /api/_auth/session. That's fine in examples like a client calling GitHub API using their access token to access their own resources. However, if substancial scopes are granted (e.g. deleting devices on Active Directory), you may want to store the access token on the server, and keep a reference to it within setUserSession() so you can access the token within requests to server routes.

victorlmneves commented 10 months ago

hi @RihanArfan I know that the question is to be able to expose it to be to access it without having to add to the object Currently, I'm doing like this, but I don't think it's safe to do it

export default oauth.auth0EventHandler({
    config: authConfig(),
    async onSuccess(event, { user, tokens }) {
        const userStore = useUserStore();
        userStore.$state.isLoggedIn = true;
        userStore.$state.user = user;

        await setUserSession(event, {
            user: {
                login: user,
            },
            loggedInAt: Date.now(),
            auth0AccessToken: tokens.access_token,
        });

        return sendRedirect(event, '/');
    },
});
RihanArfan commented 10 months ago

However, if substancial scopes are granted (e.g. deleting devices on Active Directory), you may want to store the access token on the server, and keep a reference to it within setUserSession() so you can access the token within requests to server routes.

Here's an example of what I'm doing:

// server/routes/auth.ts
export default oauth.microsoftEventHandler({
  async onSuccess(event, { user, tokens }) {
    await useStorage().setItem(`token:${user.id}`, tokens.access_token);

    await setUserSession(event, { user, loggedInAt: Date.now() });
    return sendRedirect(event, "/");
  },
});
// server/api/example.get.ts
export default defineEventHandler(async (event) => {
  const session = await requireUserSession(event);
  const accessToken = await useStorage().getItem(`token:${(session.user as { id: string }).id}`);
  // TODO: validate JWT is valid

  // TODO: your code
  ...
});

You'll still need to handle refresh tokens otherwise your application won't be able to call an API after 1 hour (most common access token JWT expiry). I'm figuring out how to handle refresh tokens still.

victorlmneves commented 10 months ago

hi @RihanArfan Thanks for the tip I was currently looking at this but going to try your approach If you figure out how to manage the refresh token, please leave a comment, and I'll do the same.

RihanArfan commented 10 months ago

@victorlmneves Make sure to include the offline_access scope, and you'll get a refresh token within tokens, assuming you've enabled the relevant settings within your Auth0 dashboard (or in my case, via Azure).

I'm working on a server util getAccessToken(session) which would handle refreshing an expired access token. I'm thinking of storing the refresh token via useStorage(), and the result of getAccessToken() using Nitro's caching layer with a TTL a little less than the JWT expiry. https://nitro.unjs.io/guide/cache#function (which uses unstorage under the hood too).

If the refresh token doesn't work (90+ days unused, user changed pw/removed device from trusted, etc. in Microsoft specifically), I'll redirect them to the auth endpoint.

Velka-DEV commented 3 weeks ago

Here is how we achieve this for our application (Zitadel provider, but applicable to any provider).

This way, the tokens are encrypted along with the user info in the session.

I think this issue can be closed now to clean the backlog. @atinux

~/server/routes/auth/zitadel.get.ts:

export default defineOAuthZitadelEventHandler({
    config: {
        scope: ["openid", "email", "profile", "offline_access"],
    },
    async onSuccess(event, { user, tokens }) {
        await setUserSession(event, {
            user: {
                zitadel: user.sub,
                name: user.name,
                avatar: user.picture,
                firstName: user.given_name,
                lastName: user.family_name,
                email: user.email,
                emailVerified: user.email_verified,
                locale: user.locale,
            },
            tokens: {
                accessToken: tokens.access_token,
                idToken: tokens.id_token,
                refreshToken: tokens.refresh_token,
                expiresAt: tokens.expires_at,
                tokenType: tokens.token_type,
            },
            loggedInAt: Date.now(),
        });

        return sendRedirect(event, "/");
    },
});

~/auth.d.ts:

// auth.d.ts
declare module "#auth-utils" {
    interface User {
        zitadel: string;
        name: string;
        avatar: string?;
        firstName: string;
        lastName: string;
        email: string;
        emailVerified: boolean;
        locale: string;
    }

    interface Tokens {
        accessToken: string;
        idToken: string;
        refreshToken: string;
        expiresAt: number;
        tokenType: string;
    }

    interface UserSession {
        user: User;
        tokens: Tokens;
        loggedInAt: number;
    }
}

export {};
atinux commented 3 weeks ago

Please use the secure object when using setUserSession to only keep the api tokens available on the server.

Example:

await setUserSession(event, {
  user: {
    zitadel: user.sub,
    name: user.name,
    avatar: user.picture,
    firstName: user.given_name,
    lastName: user.family_name,
    email: user.email,
    emailVerified: user.email_verified,
    locale: user.locale,
  },
  secure: {
    tokens: {
      accessToken: tokens.access_token,
      idToken: tokens.id_token,
      refreshToken: tokens.refresh_token,
      expiresAt: tokens.expires_at,
      tokenType: tokens.token_type,
    },
  },
  loggedInAt: Date.now(),
});
Velka-DEV commented 3 weeks ago

Please use the secure object when using setUserSession to only keep the api tokens available on the server.

Example:

await setUserSession(event, {
  user: {
    zitadel: user.sub,
    name: user.name,
    avatar: user.picture,
    firstName: user.given_name,
    lastName: user.family_name,
    email: user.email,
    emailVerified: user.email_verified,
    locale: user.locale,
  },
  secure: {
    tokens: {
      accessToken: tokens.access_token,
      idToken: tokens.id_token,
      refreshToken: tokens.refresh_token,
      expiresAt: tokens.expires_at,
      tokenType: tokens.token_type,
    },
  },
  loggedInAt: Date.now(),
});

We do rely on these tokens to call our GraphQL API, storing them server-side only is not actually something possible.

atinux commented 3 weeks ago

Well then don't put them in secure but be aware that if you have a XSS vulnerability in your app, your tokens will be leaked.