Search code examples
next.jsgoogle-apigoogle-oauthgoogle-fitnext-auth

Is it possible to add more scopes to NextAuth provider during session?


I am currently using NextAuth to signIn in my application, and want to add more scopes into it while the user is already signed in so I can use the Google Fit API.

I've been reading the documentation of NextAuth and doing some research but did not find anything helpful for the current NextAuth v4 in this scope situation.

My current Google configuration:

import NextAuth from 'next-auth';
import GoogleProvider from "next-auth/providers/google"

const GOOGLE_AUTHORIZATION_URL =
    'https://accounts.google.com/o/oauth2/v2/auth?' +
    new URLSearchParams({
        prompt: 'consent',
        access_type: 'offline',
        response_type: 'code'
    })

export default NextAuth({
    // Configure one or more authentication providers
    providers: [
        GoogleProvider({
            clientId: process.env.GOOGLE_CLIENT_ID,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET,
            authorization: GOOGLE_AUTHORIZATION_URL,
        }),
  ],
callbacks: {
        async jwt({ token, user, account }) {
            // Initial sign in
            if (account && user) {
                return {
                    accessToken: account.access_token,
                    accessTokenExpires: Date.now() + account.expires_in * 1000,
                    refreshToken: account.refresh_token,
                    user
                }
            }

            // Return previous token if the access token has not expired yet
            if (Date.now() < token.accessTokenExpires) {
                return token
            }

            // Access token has expired, try to update it
            return refreshAccessToken(token)
        },
        async session({ session, token }) {
            session.user = token.user;
            session.accessToken = token.accessToken
            session.error = token.error
            return session
        }
    },
jwt: {
        secret: process.env.NEXTAUTH_JWT_SECRET,
    },
    secret: process.env.NEXTAUTH_SECRET,
})


async function refreshAccessToken(token) {
    try {
        const url =
            "https://oauth2.googleapis.com/token?" +
            new URLSearchParams({
                client_id: process.env.GOOGLE_CLIENT_ID,
                client_secret: process.env.GOOGLE_CLIENT_SECRET,
                grant_type: "refresh_token",
                refresh_token: token.refreshToken,
            })

        const response = await fetch(url, {
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            method: "POST",
        })

        const refreshedTokens = await response.json()

        if (!response.ok) {
            throw refreshedTokens
        }

        return {
            ...token,
            accessToken: refreshedTokens.access_token,
            accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
            refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
        }
    } catch (error) {
        console.log(error)

        return {
            ...token,
            error: "RefreshAccessTokenError",
        }
    }
}

My current code is working just fine, so I just need the scopes to authorize and use the Google Fitness API.


Solution

  • Actually made it work, created a file called add_scopes.js inside pages/api/auth/

    export default (req, res) => {
        if (req.method === 'POST') {
            // construct the authorize URL with additional scopes
            const scopes = 'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.location.read'
            const redirectUri = process.env.GOOGLE_CALLBACK_URL
            const clientId = process.env.GOOGLE_CLIENT_ID
            const authorizationUrl = `https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code&scope=${scopes}&redirect_uri=${redirectUri}&client_id=${clientId}`
            // send the authorization URL to the client
            res.json({ authorizationUrl });
        } else {
            res.status(405).end(); // Method Not Allowed
        }
    }
    

    then made a button to call this api route:

    import { useCallback } from 'react';
    import { Button }  from 'react-bootstrap';
    
    
    const AddScopesButton = ({scopes=scopes}) => {
        const isAuthorized = scopes.includes("https://www.googleapis.com/auth/fitness.activity.read") && scopes.includes("https://www.googleapis.com/auth/fitness.location.read")
        const handleClick = useCallback(async () => {
            try {
                const res = await fetch("/api/auth/add_scopes", { method: "POST" });
                const json = await res.json()
                if (res.ok) {
                    window.location.href = json.authorizationUrl;
                } else {
                    throw new Error(res.statusText);
                }
            } catch (error) {
                console.error(error);
            }
        }, []);
    
        return (
            <>
                {!isAuthorized && (
                        <Button className='mt-2' onClick={handleClick}>Add Scopes</Button>
                )}
                {isAuthorized && <span>Authorized</span>}
            </>
        );
    };
    
    export default AddScopesButton;
    

    The only problem is if you signOut and signIn back in you need to get the authorization again, would really like to know if there is a way to save the accessToken/scopes that were authorized.