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.
Related
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.
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.
So all answers I've found publicly to this question weren't very helpful and while they "worked", they were incredibly hacky.
Basically I have a vuex variable, appLoading which is initially true but gets set to false once all async operations are complete. I also have another vuex variable called user which contains user information that gets dispatched from the async operation once it gets returned.
I then also have a router guard that checks;
router.beforeEach((to, from, next) => {
if (to.matched.some(route => route.meta.requiresAuth)) {
if (store.getters.getUser) {
return next();
}
return router.push({ name: 'index.signup' });
}
return next();
});
In my initial Vue instance I then display a loading state until appLoading = false;
Now this "works" but there is a problem which is really bugging me. If you load the page, you will get a "flicker" of the opposite page you are supposed to see.
So if you are logged in, on first load you will see a flicker of the signup page. If you aren't logged in, you will see a flicker of the logged in page.
This is pretty annoying and I narrowed the problem down to my auth guard.
Seems it's pushing the signup page to the router since user doesn't exist then instantly pushes to the logged in page since user gets committed.
How can I work around this in a way that isn't hacky since it's kinda annoying and it's sort of frustrating that Vue doesn't have even the slightest bit of official docs/examples for a problem as common as this, especially since such a large number of webapps use authentication.
Hopefully someone can provide some help. :)
The router beforeEach hook can be a promise and await for a Vuex action to finish. Something like:
router.beforeEach(async (to, from, next) => {
if (to.matched.some(route => route.meta.requiresAuth)) {
await store.dispatch('init');
if (store.getters.getUser) {
return next();
}
return router.push({ name: 'index.signup' });
}
return next();
});
The 'init' action should return a promise:
const actions = {
async init({commit}) {
const user = await service.getUser();
commit('setUser', user);
}
}
This approach has the problem that whenever we navigate to a given page it will trigger the 'init' action which will fetch the user from the server. We only want to fetch the user in case we don't have it, so we can update the store check if it has the user and fetch it acordingly:
const state = {
user: null
}
const actions = {
async init({commit, state}) {
if(!state.user) {
const user = await service.getUser();
commit('setUser', user);
}
}
}
As per discussion in comments:
Best approach for you case will be if you make your appLoading variable a promise. That's how you can do things or wait for things until your app data is resolved.
Considering appLoading a promise which you initialize with your api call promise, your router hook will be like:
router.beforeEach((to, from, next) => {
appLoading.then(() => {
if (to.matched.some(route => route.meta.requiresAuth)) {
if (store.getters.getUser) {
return next();
}
return router.push({ name: "index.signup" });
}
return next();
});
});
You might want to just keep it as an export from your init code instead of keeping it in Vuex. Vuex is meant for reactive data that is shared over components.
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
I want to implement the new Auth0 Lock 10 in my React/Redux app.
I've checked on the internet, but nothing matches my question. There's a tutorial here, but it uses the Popup mode instead of the Redirect (default now) mode. Another one parses the url, which is useless in Lock 10.
Here's the flow:
The Auth0Lock gets instantiated when my app starts
When the user clicks on the login button, it shows the Lock widget (lock.show()) and dispatches LOGIN_REQUEST
The lock does its authentication on auth0.com (redirects out of my localhost)
Redirect back to my localhost after successful login, the Auth0Lock get instantiated again
I wait for an lock.on('authenticated') event to dispatch LOGIN_SUCCESS
And here is my actions/index.js code:
import Auth0Lock from 'auth0-lock'
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_ERROR = 'LOGIN_ERROR'
function loginRequest() {
return {
type: LOGIN_REQUEST
}
}
function loginSuccess(profile) {
return {
type: LOGIN_SUCCESS,
profile
}
}
function loginError(error) {
return {
type: LOGIN_ERROR,
error
}
}
// import AuthService to deal with all the actions related to auth
const lock = new Auth0Lock('secret', 'secret', {
auth: {
redirectUrl: 'http://localhost:3000/callback',
responseType: 'token'
}
})
lock.on('authenticated', authResult => {
console.log('Im authenticated')
return dispatch => {
return dispatch(loginSuccess({}))
}
})
lock.on('authorization_error', error => {
return dispatch => dispatch(loginError(error))
})
export function login() {
lock.show()
return dispatch => {return dispatch(loginRequest())}
}
Now when I click on the login button, redux logger shows me LOGIN_REQUEST action dispatched, I see the lock widget, I can login, it redirects to auth0.com then back to my localhost:3000/callback with a pretty token. Everything is fine, I see the Im authenticated message in my console, but redux logger doesn't show me that the LOGIN_SUCCESS action has been dispatched.
I'm new to Redux, and I guess I'm missing one thing, but I cannot get grab of it. Thanks!
I finally put in inside actions.js, I created a new function called checkLogin()
// actions.js
const authService = new AuthService(process.env.AUTH0_CLIENT_ID, process.env.AUTH0_DOMAIN)
// Listen to authenticated event from AuthService and get the profile of the user
// Done on every page startup
export function checkLogin() {
return (dispatch) => {
// Add callback for lock's `authenticated` event
authService.lock.on('authenticated', (authResult) => {
authService.lock.getProfile(authResult.idToken, (error, profile) => {
if (error)
return dispatch(loginError(error))
AuthService.setToken(authResult.idToken) // static method
AuthService.setProfile(profile) // static method
return dispatch(loginSuccess(profile))
})
})
// Add callback for lock's `authorization_error` event
authService.lock.on('authorization_error', (error) => dispatch(loginError(error)))
}
}
And in the constructor of my App component, I call it
import React from 'react'
import HeaderContainer from '../../containers/HeaderContainer'
class App extends React.Component {
constructor(props) {
super(props)
this.props.checkLogin() // check is Auth0 lock is authenticating after login callback
}
render() {
return(
<div>
<HeaderContainer />
{this.props.children}
</div>
)
}
}
App.propTypes = {
children: React.PropTypes.element.isRequired,
checkLogin: React.PropTypes.func.isRequired
}
export default App
See here for full source code: https://github.com/amaurymartiny/react-redux-auth0-kit
My Reactjs knowledge is limited, but this was starting to be to long for a comment...
Should you not be calling store.dispatch(...) from the lock events?
Having those events return a function won't do anything unless someone invokes the function that is returned and to my knowledge Lock does not do anything with the return value of the callback function you pass as an event handler.
I think what's happening is auth0 redirects the browser window to the login authority (auth0 itself, Facebook, Google, etc.) then redirects you back to your app, which reloads your page, essentially wiping out all state. So your dispatch is sent, then the page reloads, which wipes out your state. Logging in appears to work if you use localStorage instead of redux state, but I'm not sure how that's going to affect all the other state I will need to put in my app.