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.
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'm using the default bottom tab navigation app from the expo example
My appnavigator.js looks like this
export default createAppContainer(createSwitchNavigator({
// You could add another route here for authentication.
// Read more at https://reactnavigation.org/docs/en/auth-flow.html
Main: MainTabNavigator,
}));
I want to check if the user is logged in, by checking the asyncstorage loginname value. In the home.js, if there is no loginname, then I want to redirect to the sigin.js page.
I suggest creating a file called initializing.js as a screen, which will be the first entry point in the app and put the logic there.
export default class Initializing extends Component {
async componentDidMount() {
try {
const token = await AsyncStorage.getItem('user_token');
const skipOnBoarding = true;
const authenticated = true;
if (token) await goToHome(skipOnBoarding, authenticated);
else await goToAuth();
SplashScreen.hide();
} catch (err) {
await goToAuth();
SplashScreen.hide();
}
}
render() {
return <View />;
}
}
I worked it out. was very easy.
First do this in the appNavigator.js
import SignIn from '../screens/SignIn'
export default createAppContainer(createSwitchNavigator({
// You could add another route here for authentication.
// Read more at https://reactnavigation.org/docs/en/auth-flow.html
Main: MainTabNavigator,
SignIn: SignIn // signIn is the login page
}));
Next, at the logic where you check for user logined do something like this.
AsyncStorage.getItem('loginname').then((value) => {
console.log(value)
this.props.navigation.navigate('SignIn')
})
the prop this.props.navigation.navigate is automatically avaiable in every stack, you dont need to pass it around to use it
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