I made a spotify clone which have a login,main pages. The user is initially directed to login page (localhost:3000/login) and once the user clicks login button they are taken to spotify authentication callback url (from spotify side, we can login using google, facebook or email etc) and once the user is logged in (successfully authenticated) , spotify provides a token which is used to check if the user is authenticated from client side. If success, the user is taken to the main page (which has all the music in it).
The spotify dashboard takes in redirect url for authentication which in my case its -> http://localhost:3000/api/auth/callback/spotify
This workflow worked perfectly when working and running locally ( localhost:3000 ).
When hosting in vercel,
I created a project in vercel without envs
I took my https://sp-app.vercel.app which is the domain and added to NEXTAUTH_URL into my env, and also added - NEXT_PUBLIC_CLIENT_SECRET, NEXT_PUBLIC_CLIENT_ID and JWT_SECRET
Went to spotify dashboard and editing the redirect url to https://sp-app.vercel.app/api/auth/callback/spotify and saved
redeployed the app from vercel (which gave me a deployent url, but nevermind) and clicked on the domains --> sp-app.vercel.app
The login page came, once I clicked on login button, it loads and stays in login page itself. It isn't moving to my home page nor authenticating but this same code worked locally fine.
Code to understand :
Login button:
{providers !== null && Object.values(providers).map((provider) => (
<div key={provider.name}>
<button onClick={()=>signIn(provider.id, {callbackUrl:"/"})}
>Login with {provider.name}</button>
</div>
}
export async function getServerSideProps(){
const providers = await getProviders(); //getProviders is imported from next-auth
return {
props:{
providers,
}
}
}
middleware:
export async function middleware(req){
const token = await getToken({req,secret:process.env.JWT_SECRET});
const {pathname} = req.nextUrl;
//Allow the request if,
// (1) Its a request for next-auth session & provider fetching
// (2) The token exists
if(pathname.includes('/api/auth') || token){
return NextResponse.next();
}
//Redirect to login if no token or requesting a protected route
if(!token && pathname !== "/login"){
return NextResponse.redirect("/login");
}
}
While I checked with Network tab, I get a session which should log me in and redirect to main page, but it isn't working in vercel deployment.
Hey so first off make sure your NEXTAUTH_URL environment variable is correct. Also check that the redirect URI (from the Spotify developer dashboard) is correct.
After that, add this secureCookie code to the getToken function in the _middleware.js file. So that function should end up looking like this:
const token = await getToken({
req,
secret: process.env.JWT_SECRET,
secureCookie:
process.env.NEXTAUTH_URL?.startsWith("https://") ??
!!process.env.VERCEL_URL,
});
Let me know if this works. I had the same exact issue and this worked for me.
Related
I do have a super weird error coming up only when deploying the code to Vercel. It doesn't happen locally which makes it quite annoying to begin with.
I do have a staging and a production instance for my code. I want to protect the staging with a password which is not difficult since I implemented the authentication via Firebase. The only tricky part is that I don't use Firebase to keep track of the user but my server (basically setting a cookie). I should mention that I am using Sveltekit to put it all together.
In sveltekit you can use hooks, which can be seen as middlewares, to redirect a user to the sign-in page if the env variable for the environment is set to dev.
Another hook redirects a logged-in user, so if you are already logged in and try to go to auth/sign-in or auth/sign-up you'll get redirected to the home page.
Now the weird happens: I go on the deployed version of the site, and I get immediately redirected to the sign-in page, which is correct. I try to navigate to all the pages of the website, the redirect still works fine. I log in and upon success, I should be redirected to the homepage, which I do BUT the home page redirects me to the sign-in page as if I wasn't logged in and again the sign-in page redirects me to the home page as if I was, thus creating a loop.
I honestly don't know why this happens since it perfectly works locally, so my thoughts go to Vercel. I would exclude Firebase since I remembered to put the custom domain as an allowed domain in the settings.
To give a bitmore context, I structured the hooks responsible for the redirect in this way:
export const authSessionHandler: Handle = async ({ event, resolve }) => {
const cookie = event.locals.cookie;
const idToken = await getIdTokenFromSessionCookie(getCookieValue(cookie, 'session'));
const user = idToken
? {
uid: idToken?.sub,
email: idToken?.email
}
: null;
event.locals.idToken = idToken;
event.locals.user = user;
return resolve(event);
};
export const redirectLoggedInUserHandler: Handle = async ({ event, resolve }) => {
const { user } = event.locals;
const next = event.url.searchParams.get('next') || '/';
if (
user &&
(event.url.pathname.startsWith('/auth/sign-in') ||
event.url.pathname.startsWith('/auth/sign-up'))
) {
return new Response('Redirect', {
status: http_302.status,
headers: {
location: `${next}`
}
});
}
return resolve(event);
};
export const redirectToSignInForDevEnvironmentHandler: Handle = async ({ event, resolve }) => {
const { user } = event.locals;
const allowedEndpoints = ['/auth/sign-in', '/auth/session'];
if (!user && env === 'dev' && !allowedEndpoints.includes(event.url.pathname)) {
return new Response('Redirect', {
status: http_302.status,
headers: {
location: '/auth/sign-in'
}
});
}
return resolve(event);
};
The handlers are in that order, so the first one populates the user and the rest can check the rest.
In the code I am getting the user from event.locals which kind of decides the entire logic (as it should) and to me it's quite interesting and telling the fact that the sign-in page redirects me to home which mean the user is defined, but the home page redirects back as if the user was not defined. This made me think it is not a problem with the code but probably the provider(s) Vercel or Firebase.
It would be very helpful to know your thoughts about it.
I am trying to implement magic link login to my app. I enabled email login option through Firebase console and localhost is already under the authorized domains. I have the code snippet and the screenshot in the below.
I can see that some request is being done with 200 success code but I receive no email.The code does not throw any error and I have no idea what is wrong at this point. Can someone help?
export const sendMagicLink = (email: string, redirectUrl: string) => {
const auth = getAuth(getClientApp());
const actionCodeSettings = {
url: redirectUrl,
handleCodeInApp: true
};
return sendSignInLinkToEmail(auth, email, actionCodeSettings);};
const handleSubmit: svelte.JSX.EventHandler<SubmitEvent, HTMLFormElement> = async ({
currentTarget
}) => {
email = new FormData(currentTarget).get('email') as string;
const redirectUrl = `${window.location.origin}/auth/confirm`;
state = 'submitting';
try {
await sendMagicLink(email, redirectUrl);
setMagicEmail(email);
state = 'success';
} catch (error) {
if (error instanceof Error) {
state = error;
} else {
console.log(error);
state = new Error('something went wrong sending the magic link 😞');
}
}
};
Request body:
canHandleCodeInApp true
continueUrl "http://localhost:3000/auth/confirm"
email "someemail#gmail.com"
requestType "EMAIL_SIGNIN"
Intuitively a developer assumes that emails sent out by Firebase's internal email service will not be classified as spam, but this happens very often.
To solve this, one would need to:
Setup a custom domain for Authentication in Firebase Console
Go to Firebase Authentication
Go to Templates
Go to Email Address Verification
Click Edit
Click Customize domain and go through the whole process
Setup a proper SMTP server in Firebase Console
Go to Authentication
Go to Templates
Go to SMTP Settings and enter SMTP Settings. Use the same sender domain as has been used in Email Address Verification above.
Setting Action URL
Set your custom domain in the Hosting section, first, e.g.: example.com.
Then, in the Authorization Templates section, click Edit and adjust the Custom Action URL at the bottom of the page. Set it to the same domain used for Hosting, e.g.:
https://example.com/__/auth/action
This helps to decrease the spam ranking of the emails, as the outgoing email from domain A will now contain a link to domain A.
In contrast, an email from domain A carrying a link to domain B is more suspicious.
We're using Firebase in a Next.js app at work. I'm new to both, but did my best to read up on both. My problem is more with Firebase, not so much with Next.js. Here's the context:
In the client app, I make some calls to our API, passing a JWT (the ID token) in an Authorization header. The API calls admin.auth().verifyIdToken to check that the ID token is fresh enough. This works fine, since I am more or less guaranteed that the ID token gets refreshed regularly (through the use of onIDTokenChanged (doc link)
Now I want to be able to Server-Side Render my app pages. In order to do that, I store the ID token in a cookie readable by the server. But from here on, I have no guarantee that the ID token will be fresh enough next time the user loads the app through a full page load.
I cannot find a server-side equivalent of onIDTokenChanged.
This blog post mentions a google API endpoint to refresh a token. I could hit it from the server and give it a refresh token, but it feels like I'm stepping out of the Firebase realm completely and I'm worried maintaining an ad-hoc system will be a burden.
So my question is, how do people usually reconcile Firebase auth with SSR? Am I missing something?
Thank you!
I've had that same problem recently, and I solved by handling it myself. I created a very simple page responsible for forcing firebase token refresh, and redirecting user back to the requested page. It's something like this:
On the server-side, check for token exp value after extracting it from cookies (If you're using firebase-admin on that server, it will probably tell you as an error after verifying it)
// Could be a handler like this
const handleTokenCookie = (context) => {
try {
const token = parseTokenFromCookie(context.req.headers.cookie)
await verifyToken(token)
} catch (err) {
if (err.name === 'TokenExpired') {
// If expired, user will be redirected to /refresh page, which will force a client-side
// token refresh, and then redirect user back to the desired page
const encodedPath = encodeURIComponent(context.req.url)
context.res.writeHead(302, {
// Note that encoding avoids URI problems, and `req.url` will also
// keep any query params intact
Location: `/refresh?redirect=${encodedPath}`
})
context.res.end()
} else {
// Other authorization errors...
}
}
}
This handler can be used on the /pages, like this
// /pages/any-page.js
export async function getServerSideProps (context) {
const token = await handleTokenCookie(context)
if (!token) {
// Token is invalid! User is being redirected to /refresh page
return {}
}
// Your code...
}
Now you need to create a simple /refresh page, responsible for forcing firebase token refresh on client-side, and after both token and cookie are updated, it should redirect user back to the desired page.
// /pages/refresh.js
const Refresh = () => {
// This hook is something like https://github.com/vercel/next.js/blob/canary/examples/with-firebase-authentication/utils/auth/useUser.js
const { user } = useUser()
React.useEffect(function forceTokenRefresh () {
// You should also handle the case where currentUser is still being loaded
currentUser
.getIdToken(true) // true will force token refresh
.then(() => {
// Updates user cookie
setUserCookie(currentUser)
// Redirect back to where it was
const decodedPath = window.decodeURIComponent(Router.query.redirect)
Router.replace(decodedPath)
})
.catch(() => {
// If any error happens on refresh, redirect to home
Router.replace('/')
})
}, [currentUser])
return (
// Show a simple loading while refreshing token?
<LoadingComponent />
)
}
export default Refresh
Of course it will delay the user's first request if the token is expired, but it ensures a valid token without forcing user to login again.
First of all, I am using nodejs for the backend. I use firebase hosting and firebase functions to deploy an express() app.
What I am trying to achieve is to make an admin website, which is connected to Firebase. so I have a route /admin/ like this:
adminApp.get("/", (request, response) => {
return response.redirect("/admin/login");
});
Here I basically want to check if a current user is logged in - or not.
I know firebase supports client side authentication using:
firebase.auth().onAuthStateChanged(user => {
if (user) {
} else {
}
});
And using
function login() {
var userEmail = document.getElementById("email").value;
var userPass = document.getElementById("password").value;
firebase.auth().signInWithEmailAndPassword(userEmail, userPass).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
if (error) {
document.getElementById('loginError').innerHTML = `Error signing in to firebase`;
}
});
}
However image this case:
Someone (not an admin) is visiting /admin/some_secret_website/ which he obviously does not have access to.
If I rely on client side authentication, it first loads the entire website and the scripts and then notices - hey I am not authenticated, let me redirect to /login. By then however anyone knows the source code of an admin page.
I'd rather have something like:
adminApp.get("/admin/some_secret_website", (request, response) => {
if (request.user) {
// user is authenticated we can check if the user is an admin and give access to the admin page
}
});
I know that you can get the user's token and validate that token using the AdminSDK, but the token must be send by the client code, meaning the website was already loaded.
I came across Authorized HTTPS Endpoint by firebase, but it only allows a middleware when using a bearer token.
Does anybody know how I can maintain a server side user object to not even return admin html to the browser but only allow access to admins?
Like Doug indicated, the way your admin website/webapp would function with Firebase Cloud Functions (which is effectively a Nodejs server) is that you get the request, then use the headers token to authenticate them against Firebase Auth. See this answer for a code snippet on this.
In your case, I'm thinking you would create a custom claim for an "administrator" group and use that to determine whether to send a pug templated page as a response upon authentication. As far as Authorization, your db rules will determine what said user can CRUD.
Follow code worked fine with a user already authenticated with my facebook application. But throw a error: "This webpage has a redirect loop" when use a new user.
var myRootRef = new Firebase('https://tttb-demo.firebaseio.com/');
var auth = new FirebaseSimpleLogin(myRootRef, function (error, user) {
});
auth.login('facebook', {
rememberMe: true,
scope: 'email,read_friendlists'
});
I had this problem.
Either:
1) Take your app out of Sandbox mode on Facebook
or
2) Add the user to the list of developers on Facebook.
It's because only you are authorised to access it by default when you create an app in Facebook.