The route I want to protect: /account
If the user is NOT authenticated, then redirect to /signIn
Having an SSR NextJS project, and working with Firebase authentication, how can I achieve a production battle-tested proper protected routes?
The example provided on NextJS docs is not working right now:
with-firebase-auth
So I submitted an issue:
with-firebase-auth-example-not-working
Add to that that I'm new to NextJs and also, unfortunately, I've never used JWT :( or any sort of backend protected routes cookies/JWT/sessions implementation....Until now that I want/need it.
What sort of workaround I've tried, well, something like this:
import Account from "./Account.js";
import Loading from "./Loading.js";
import { useRequireAuth } from "./use-require-auth.js";
function Account(props) {
const auth = useRequireAuth();
// If auth is null (still fetching data)
// or false (logged out, above hook will redirect)
// then show loading indicator.
if (!auth) {
return <Loading />;
}
return (
<Account auth={auth} />
);
}
// Hook (use-require-auth.js)
import { useEffect } from "react";
import { useAuth } from "./use-auth.js";
import { useRouter } from "./use-router.js";
function useRequireAuth(redirectUrl = '/sigIn'){
const auth = useAuth();
const router = useRouter();
// If auth.user is false that means we're not
// logged in and should redirect.
useEffect(() => {
if (auth.user === false){
router.push(redirectUrl);
}
}, [auth, router]);
return auth;
}
But this is all happening on the client-side....the server is not checking anything.
I'm gonna a post a very basic answer to this. I dunno how you're going to check if a user is authenticated on firebase. My own code uses AWS Cognito for this purpose.
We' are going to put that piece of code at the end of the page. By doing so, if the user is not authenticated we will redirect the user to the sign in page.
export async function isAuthenticated(context) {
// your code to check firebase authentication
// return true if not authenticated, else return false
// Maybe this way
var user = firebase.auth().currentUser;
if (user)
return false;
else
return true;
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
let shouldRedirect = await isAuthenticated(ctx);
if (shouldRedirect) {
return {
redirect: {
destination: '/sign-in',
permanent: false
}
}
}
return {
props: {}
}
}
export default Account;
That's it. Now the route is protected through SSR.
Related
I'm currently building a blog sample app, using NextJS, ApolloClient and MongoDB + MongoRealm. The NextJS skeleton was built after the framework's official page tutorial.
At the moment, new users can signup, by accessing a SignUp form which is routed at 'pages/signup'. After entering their credentials, they are redirected to the home page. Then, the freshly signed in users have to visit another page(the one associated with 'pages/login' root), which contains the login form, which is responsible with their email/password authentication.
Also, I've set up Realm to send a confirmation email at the user's email address. The email contains a link to a customized page from my NextJs app, which will handle their confirmation(users also have to be confirmed, after requesting a sign in)
The workflow should be established with this. However, I want to automatically login a user, after he/she just logged in(so that they won't need to sign in and also visit the log in page, when creating their accounts).
The problem I'm encountering is that my React component that handles the user confirmation, doesn't have access to the user instance's email and password. I need a way to login the user, without having access to his/her credentials.
Below, I will try to explain exactly why this access restriction happens in the first place. Although the entire '_app.js' is wrapped in some custom providers, I'll try to keep things as simple as possible, so I'll present only what is needed for this topic.
My signup.js file looks something like this:
import { useForm } from "react-hook-form";
// Used 'useForm' hook to simplify data extraction from the //input form
import { useAuth } from "members";
const SignUpForm = () => {
const router = useRouter();
const { handleSubmit, register } = useForm();
const { signup } = useAuth();
const signUpAndRedirect = (form) => {
signup(form.email, form.password);
router.push("/");
// after signing up, redirect client back to home
};
return (
{/*My form's 'email' and 'password' fields are only accessible in the SignUpForm component*/}
<div>
<form onSubmit={handleSubmit(signUpAndRedirect)}>
...
...
</form>
</div>
);
};
export default SignUpForm;
My login.js file is built after the same concept, the only difference being that 'signUpAndRedirect' is replaced with
'authenticateAndRedirect':
const authenticateAndRedirect = (form) => {
login(form.email, form.password);
router.push("/");
};
And here is my confirm.js file, which is responsible with extracting the token and tokenId from the confirmation URL. This component is normally only rendered when the client receives the email and clicks on the confirmation link(which basically has the form /confirm, where each token is a string and is added into the URL by Realm).
import Link from "next/link";
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useAuth } from "members";
const Confirm = () => {
const router = useRouter();
const { confirm, login } = useAuth();
useEffect(() => {
const token = router.query.token;
const tokenId = router.query.tokenId;
if (token && tokenId) {
confirm(token, tokenId);
login(email, password); // !!! I don't have access to these
}
}, [router]);
//used useEffect() to assure the confirmation only happens once, after the component was rendered.
return (
<div>
<h2>
Thank you for confirming your email. Your profile was successfully
activated.
</h2>
<Link href="/">
<a>Go back to home</a>
</Link>
</div>
);
};
export default Confirm;
And finally, just a quick look into the signup, login and confirm methods that I have access to through my customized providers. I am quite positive that they work correctly:
const client = () => {
const { app, credentials } = useRealm();
const [currentUser, setCurrentUser] = useState(app.currentUser || false);
const [isAuthenticated, setIsAuthenticated] = useState(user ? true : false);
// Login and logout using email/password.
const login = async (email, password) => {
try {
const userCredentials = await credentials(email, password);
await app.logIn(userCredentials);
setCurrentUser(app.currentUser);
setIsAuthenticated(true);
} catch (e) {
throw e;
}
};
const logout = async () => {
try {
setUser(null);
// Sign out from Realm and Auth0.
await app.currentUser?.logOut();
// Update the user object.
setCurrentUser(app.currentUser);
setIsAuthenticated(false);
setUser(false);
} catch (e) {
throw e;
}
};
const signup = async (email, password) => {
try {
await app.emailPasswordAuth.registerUser(email, password);
// await app.emailPasswordAuth.resendConfirmation(email);
} catch (e) {
throw e;
}
};
const confirm = async (token, tokenId) => {
try {
await app.emailPasswordAuth.confirmUser(token, tokenId);
} catch (e) {
throw e;
}
};
return {
currentUser,
login,
logout,
signup,
confirm,
};
};
export default client;
The currentUser will basically represent the Realm.app.currentUser and will be provided to the _app by my providers.
So, the problem is that my Confirm component doesn't have access to the email and password fields.
I've tried to use the useContext hook, to pass data between sibling components, but quickly abandoned this approach, because I don't want to pass sensitive data throughout my NextJS pages(The only place where I should use the password is during the MongoDB POST request, since it gets encrypted by Realm Web).
Is there any way I could solve this issue? Maybe an entirely different approach?
Thank you very much in advance! Any help would be very much appreciated!
If you disable the user email confirmation, you could potentially call the login function when the register is finished like that :
registerAndLogin(email, password)
.then(() =>
loginAndRedirect(email, password)
.then(() => router.push('/')
.catch(err => throw err)
)
.catch(err => throw err)
I used your post to resolve an error I had, so thank you by the way.
Hope my answer works, I didn't had the time to test.
What is the correct pattern to implement Auth0 route guards in Nuxt?
I've adapted the Auth0 sample code to create the following middleware:
import {getInstance} from '~/plugins/auth';
export default function () {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (!authService.isAuthenticated) {
authService.loginWithRedirect({
appState: {targetUrl: 'http://localhost:3000'},
});
}
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch('loading', loading => {
if (loading === false) {
return fn();
}
});
}
Notice that before the authentication status of Auth0 can be accessed, we must wait for the the instance to finish loading. The Auth0 sample code does this by using $watch.
My middleware code "works" but has the issue of briefly displaying the protected pages before the async $watch triggers. Is there any way to wait and block the route from continuing to render until Auth0 has finished loading and its auth status can be accessed?
I've also tried using almost the exact same code Auth0 provides without my own modifications within the beforeRouteEnter hook of the Nuxt pages. This has the same issue which begs the question as to why the Auth0 example presumably works in VueJS using beforeRouteEnter but not in Nuxt?
Solved it!
A middleware can be asynchronous. To do this return a Promise or use async/await.
https://nuxtjs.org/docs/2.x/directory-structure/middleware/
I simply wrapped my middleware script in a promise. I resolved it if the user is able to pass, otherwise I redirected them to the Auth0 login.
import {getInstance} from '~/plugins/auth';
export default function () {
return new Promise(resolve => {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (!authService.isAuthenticated) {
return authService.loginWithRedirect({
appState: {targetUrl: 'http://localhost:3000'},
});
}
resolve();
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch('loading', loading => {
if (loading === false) {
return fn();
}
});
});
}
It was also important to return the loginWithRedirect to make sure that it didn't go on to resolve the promise outside of the if block.
I'm using express + passport + nextjs to set up an app that will perform authentication using OpenID Connect. The user data is stored on the request object using express-session which gives me req.user on every request as usual.
Now I want to pass the user information to the front-end so that I can use it for something, but there does not seem to be any consistent way to do this for all requests. I can use getServerSideProps for individual pages, but not for every page through either _document or _app. How can I set this up?
Here is my current _document.tsx
import Document, {
Head,
Main,
NextScript,
DocumentContext,
} from "next/document"
export default class Doc extends Document {
public static async getInitialProps(ctx: DocumentContext) {
const req: any = ctx.req
console.log("req/user", `${!!req}/${!!(req && req.user)}`)
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
user: req?.user || "no user",
}
}
public render() {
return (
<html>
<Head />
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}
It appears to return a request object only during the very first request, not any subsequent refreshes of the page.
I've created a small repo that reproduces the issue here: https://github.com/rudfoss/next-server-custom-req
It seems ridiculous that there is no way to do this for all pages in an easy manner.
Edit: For reference this is my server.js. It is the only other relevant file in the repo
const express = require("express")
const next = require("next")
const dev = process.env.NODE_ENV !== "production"
const start = async () => {
console.log("booting...")
const server = express()
const app = next({ dev, dir: __dirname })
const handle = app.getRequestHandler()
await app.prepare()
server.use((req, res, next) => {
req.user = {
authenticated: false,
name: "John Doe",
}
next()
})
server.get("*", handle)
server.listen(3000, (err) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log("ready")
})
}
start().catch((error) => {
console.error(error)
process.exit(1)
})
It is recommended to do this via function components, as seen in the Next.js custom App docs:
// /pages/_app.tsx
import App, { AppProps, AppContext } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (appContext: AppContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext)
const req = appContext.ctx.req
return {
pageProps: {
...appProps.pageProps,
user: req?.user,
},
}
}
As in your answer, this will run on every request though so automatic static optimization will not be active.
Try a demo of changing pageProps in MyApp.getInitialProps (without usage of req.user) on the following CodeSandbox:
https://codesandbox.io/s/competent-thompson-l9r1u?file=/pages/_app.js
Turns out I can override getInitialProps on _app to make this work:
class MyApp extends App {
public static async getInitialProps({
ctx
}: AppContext): Promise<AppInitialProps> {
const req: any = ctx.req
return {
pageProps: {
user: req?.user
}
}
}
public render() {
//...
}
}
This will run on every request though so static optimization will not work, but in my case I need the information so I'm willing to accept the trade-off.
Edit: This answer also works, but it uses the "old" class-based component syntax which is no longer recommended. See answer from Karl for a more modern version using functional-component syntax.
I also had the similar problem where I had to fetch loggedIn user details from my Auth api. I solved it by wrapping my whole app inside a context provider, then using a set function for the initialState, which will remember if it was called before and fetch user details only once. Then in my each page, wherever I require these user details, I used the context to see if details are available and call the set function if details are not available. This way I think I achieved:
Only one request to fetch user details
Because it happens from the client side, TTFB is better
I can still take advantage of getStaticProps and getServerSideProps where it is required.
I am trying to access user data after succesfull login and also from every component.This is how i login the user:
router.post("/user/login",(req,res,next)=>{
passport.authenticate('local', function(err, user, info) {
console.log(JSON.stringify(user)) // here i got user data
if (err) { return next(err); }
if (!user) { return res.send("fail"); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.send("success") // when client gets success message,it will do Router.push("/dashboard")
});
})(req, res, next);
})
Server configuration:
server.use(cookieParser())
server.use(bodyParser.json())
server.use(passport.initialize());
server.use(session({
secret: 'jumpingdonger',
resave: true,
saveUninitialized: true,
cookie : { secure : false, maxAge : (4 * 60 * 60 * 1000) }, // 4 hours
}))
//Passport Middleware
server.use(passport.initialize())
server.use(passport.session())
DashBoard page:
import Header from '../components/Header'
import Layout from '../components/MyLayout.js'
import React, { Component } from 'react';
import axios from 'axios';
import Router from 'next/router'
export default class DashBoard extends Component{
constructor(props) {
super(props);
this.state = {
};
}
componentWillMount(){
console.log(this.props)
}
logout =()=>{
axios.get('/api/user/logout').then((result)=>{
if(result.data=="success"){
Router.push("/")
}
})
}
render(){
if(this.props.userData == undefined){
return(
<Layout>
<p>First you must login!</p>
</Layout>
)
}else{
return(
<Layout>
<p>Welcome to your dashboard {this.props.userData.userName}</p>
<a onClick={this.logout}>logout</a>
</Layout>
)
}
}
}
DashBoard.getInitialProps = async function(context) {
if(context.req !=undefined){ // which means that we are from server
return {
userData:context.req.user
};
}else{
return{}
}
}
So after succesfull login,i am redirected to dashboard page.Now in dashboard component i am trying to get userdata on getInitialProps.The problem is since i am redirected by client using Router.push(),i cant get data on getInitialProps unless i dont refresh the page from browser.If i refresh,context.req.user is populated by user data.So how do i access userdata with client side routing from dashboard page and also every other page ? What i couldnt understand is, is this puting user data into a cookie which i can access from client or is this a server side session ?
thanks
EDIT:
I solved the problem by transfering userData from server to client on succesfull login and seting a cookie in client side.I wonder is this a correct solution in this case ?
If user is logged in. passport will create user object in req for every request in express.
if (req.user) {
// logged in
} else {
// not logged in
}
In next.js when you use getInitialProps you have access to server with context.req.
like this:
static async getInitialProps({req}){
if(req){
// called on server
// here you can check for that user object
} else {
// called on client
}
}
However this cod will work if you have a SSR request in your next.js app (refresh page or put url and press enter).
You authenticate in server, so there is no way to find out if you have the same in front-end, Unless you use the token and verify the token in front-end as well.
Have a look at Express session which will help you with cookies.
You will need to use a redux store to solve this problem. If you have set up your redux store, you need to hydrate that into your redux store.
In your _app.js file add this code. Your pages can connect to the store.
componentDidMount () {
const { pageProps: { user }, reduxStore: { dispatch } } = this.props
if (user) {
dispatch({
type: FETCH_USER,
payload: user
})
}
}
I'm implementing the Keycloak authentication service in my Angular 2 project.
I use a service for logging in, logging out etc.
Authenticating a user and logging out seems to work. I'm now trying to protect some routes. I now have a working AuthGuard.
To check if the user is logged in (in the AuthGuard), I have a isAuthenticated() method in the service.
This is the service:
import { Injectable } from '#angular/core';
declare let Keycloak: any;
#Injectable()
export class KeycloakService {
private keycloak = new Keycloak('app/keycloak/keycloak.json');
constructor() {
this.keycloak.init({onload: 'check-sso'});
console.log(this.keycloak);
}
public login() {
this.keycloak.login();
}
public logout() {
this.keycloak.logout();
}
public isAuthenticated() {
return this.keycloak.authenticated;
}
}
Flow: User logs in, user tries to reach protected route, AuthGuard checks if user is logged in via isAuthenticated().
Note: I don't want to authenticate the user for the complete Angular app. Only for some routes.
Problem
After the user logs in, the user is redirected to the Angular app. After this, the isAuthenticated() method returns still false. Here is why:
I logged the Keycloak object to the console. I found something I didn't understand.
Keycloak object after login redirect
Same Keycloak object after login redirect (but expanded)
First the authenticated property is false. After expanding the authenticated property is true.
Question
Is the way I try to maintain my Keycloak object the correct way?
Consulted sources
https://keycloak.gitbooks.io/securing-client-applications-guide/content/v/2.5/topics/oidc/javascript-adapter.html
https://github.com/keycloak/keycloak/tree/master/examples/demo-template/angular2-product-app/src/main/webapp/app
And others
Basing on the community provided Angular2 example in keycloak's github you can spot some differences in interacting with keycloak js adapter.
Mainly the actual check on the authenticated (and possibly userName) is done on the promise returned from init.
static init(): Promise<any> {
let keycloakAuth: any = new Keycloak('keycloak.json');
KeycloakService.auth.loggedIn = false;
return new Promise((resolve, reject) => {
keycloakAuth.init({ onLoad: 'login-required' })
.success(() => {
KeycloakService.auth.loggedIn = true;
KeycloakService.auth.authz = keycloakAuth;
KeycloakService.auth.logoutUrl = keycloakAuth.authServerUrl + "/realms/demo/protocol/openid-connect/logout?redirect_uri=/angular2-product/index.html";
resolve();
})
.error(() => {
reject();
});
});
}
Also the official keycloak js adapter's documentation uses promise for the authenticated check
<head>
<script src="keycloak.js"></script>
<script>
var keycloak = Keycloak();
keycloak.init().success(function(authenticated) {
alert(authenticated ? 'authenticated' : 'not authenticated');
}).error(function() {
alert('failed to initialize');
});
</script>
</head>
If you use check-sso as a parameter to init function, the browser will be routed back to the application if the user is not logged in and will remain unauthenticated.You should use login-required instead to fix this problem.
If you don't want to authenticate the user for the complete App, you should detach the logic of creating the adapter, to make things easier if you have more than one secured component. for exemple you can create a HOC.
PS : in the example below, I am using Reactjs, I hope you can find a similar way to do this in angular:
export default (WrappedComponent) => {
return (props) => {
const [isAutenticated, setIsAutenticated] = useState(false);
const [keycloak, setKeycloak] = useState();
const loadConfig = useCallback(() => {
const keycloak = Keycloak("/keycloak.json"); //The configuration of the adapter in JSON format
keycloak.init({ onLoad: "login-required" }).then((authenticated) => {
setKeycloak(keycloak);
setIsAutenticated(authenticated);
});
}, [Keycloak]);
useEffect(() => {
loadConfig();
}, [loadConfig]);
if (keycloak) {
if (isAutenticated) {
return <WrappedComponent {...props} keycloak={keycloak} />;
} else return <AuthError message="Unable to authenticate" />;
}
return <Loader />;
};
};
you can find a useful source here