-
Notifications
You must be signed in to change notification settings - Fork 27k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add login / authentication example #153
Comments
Suggestion: Use Redux and JWT to accomplish the example |
Im working on an example for this. Currently having issues getting componentWillReceiveProps to fire on my high level component (where Im planning to check if user is authenticated and redirect to login page if not) |
So I have auth working swimmingly. As mentioned elsewhere, it's client-side only, which is ultimately just half the battle. "Pretty-secure"Like php, the atomic unit of Next is the page. One of the coolest features is that it lazy loads each page only when it's requested. With client-side only auth but with server-rendering, the js for that protected page is in fact downloaded by the browser. In the future when Next adds server workflows, you'll hopefully be able to block render and redirect on the server to prevent this entirely. This will require cookies, sessions, and AFAIK session stores, but that's just the cost of doing hybrid apps like these. Auth ExampleAssume you have a JWT-secured API with two endpoints of interest: // utils/AuthService.js
export default class AuthService {
constructor(domain) {
this.domain = domain || 'http://localhost:5000'
this.fetch = this.fetch.bind(this)
this.login = this.login.bind(this)
this.getProfile = this.getProfile.bind(this)
}
login(email, password) {
// Get a token
return this.fetch(`${this.domain}/token`, {
method: 'POST',
body: JSON.stringify({
email,
password
})
}).then(res => {
this.setToken(res.id_token)
return this.fetch(`${this.domain}/user`, {
method: 'GET'
})
}).then(res => {
this.setProfile(res)
return Promise.resolve(res)
})
}
loggedIn(){
// Checks if there is a saved token and it's still valid
const token = this.getToken()
return !!token && !isTokenExpired(token) // handwaiving here
}
setProfile(profile){
// Saves profile data to localStorage
localStorage.setItem('profile', JSON.stringify(profile))
}
getProfile(){
// Retrieves the profile data from localStorage
const profile = localStorage.getItem('profile')
return profile ? JSON.parse(localStorage.profile) : {}
}
setToken(idToken){
// Saves user token to localStorage
localStorage.setItem('id_token', idToken)
}
getToken(){
// Retrieves the user token from localStorage
return localStorage.getItem('id_token')
}
logout(){
// Clear user token and profile data from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
}
_checkStatus(response) {
// raises an error in case response status is not a success
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}
fetch(url, options){
// performs api calls sending the required authentication headers
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
if (this.loggedIn()){
headers['Authorization'] = 'Bearer ' + this.getToken()
}
return fetch(url, {
headers,
...options
})
.then(this._checkStatus)
.then(response => response.json())
}
} Next up is a HOC to make protecting pages simpler. To prevent an unwanted flash of sensitive info, the page will server-render // utils/withAuth.js - a HOC for protected pages
import React, {Component} from 'react'
import AuthService from './auth'
export default function withAuth(AuthComponent) {
const Auth = new AuthService('http://localhost:5000')
return class Authenticated extends Component {
constructor(props) {
super(props)
this.state = {
isLoading: true
};
}
componentDidMount () {
if (!Auth.loggedIn()) {
this.props.url.replaceTo('/')
}
this.setState({ isLoading: false })
}
render() {
return (
<div>
{this.state.isLoading ? (
<div>LOADING....</div>
) : (
<AuthComponent {...this.props} auth={Auth} />
)}
</div>
)
}
}
} // ./pages/dashboard.js
// example of a protected page
import React from 'react'
import withAuth from '../utils/withAuth'
class Dashboard extends Component {
render() {
const user = this.props.auth.getProfile()
return (
<div>Current user: {user.email}</div>
)
}
}
export default withAuth(Dashboard) The login page can't use the HOC as it stands now, because Login needs be public. So it just makes an instance of AuthService directly. You would do something similar for a Signup page too. // ./pages/login.js
import React, {Component} from 'react'
import AuthService from '../utils/AuthService'
const auth = new AuthService('http://localhost:5000')
class Login extends Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
componentDidMount () {
if (auth.loggedIn()) {
this.props.url.replaceTo('/admin') // redirect if you're already logged in
}
}
handleSubmit (e) {
e.preventDefault()
// yay uncontrolled forms!
auth.login(this.refs.email.value, this.refs.password.value)
.then(res => {
console.log(res)
this.props.url.replaceTo('/admin')
})
.catch(e => console.log(e)) // you would show/hide error messages with component state here
}
render () {
return (
<div>
Login
<form onSubmit={this.handleSubmit} >
<input type="text" ref="email"/>
<input type="password" ref="password"/>
<input type="submit" value="Submit"/>
</form>
</div>
)
}
}
export default Login Inspired by Airbnb's react-with-styles, I also started working on a // ./utils/withAuth.js
import nextAuth from 'next/auth'
import parseScopes from './parseScopes'
const Loading = () => <div>Loading...</div>
export default nextAuth({
url: 'http://localhost:5000',
tokenEndpoint: '/api/token',
profileEndpoint: '/api/me',
getTokenFromResponse: (res) => res.id_token,
getProfileFromResponse: (res) => res,
parseScopes,
}) Doing this all with Redux seemed unnecessarily complicated, but basically you can follow the wiki example, but move AuthService into Actions (login and logout) and have a User Reducer. You could only call these actions on the client though, since there isn't localStorage on the server, so you need to check for that in your Actions. Ultimately, redux store is put on the Lastly, assuming |
Excited to try this out! Thanks for the barebones implementation :) |
@jaredpalmer I'm working on something similar. How does your |
@amccloud It doesn't. That's the whole issue. The HOC renders |
I used cookie-js to set a cookie, but it's a bit of a hack.. |
@jaredpalmer this is great! thanks for the effort. I'll try to finish implementing your example (or help you doing it if you want) in the following days |
Yo! I published an example with nextjs and auth0 here: https://github.com/luisrudge/next.js-auth0 |
@luisrudge amazing. I'm cloning and doing some changes but looks great |
Cool! What do you think it's missing? What changes are you thinking? On Sun, Nov 6, 2016 at 1:12 PM -0200, "Dan Zajdband" <notifications@github.commailto:notifications@github.com> wrote: @luisrudgehttps://github.com/luisrudge amazing. I'm cloning and doing some changes but looks great You are receiving this because you were mentioned. |
I'll send you a pr :) |
What do you mean by multi tab support? On Sun, Nov 6, 2016 at 1:16 PM -0200, "Dan Zajdband" <notifications@github.commailto:notifications@github.com> wrote:
I'll send you a pr :) You are receiving this because you were mentioned. |
You have 2 open tabs, logout on 1, automatically logs out on the other |
Ahh. That's super cool! On Sun, Nov 6, 2016 at 1:21 PM -0200, "Dan Zajdband" <notifications@github.commailto:notifications@github.com> wrote: You have 2 open tabs, logout on 1, automatically logs out on the others You are receiving this because you were mentioned. |
Hi @luisrudge I sent you a PR with the changes https://github.com/luisrudge/next.js-auth0/pull/2 thank you so much for doing this <3 |
@impronunciable @luisrudge Fantastic implementation! If you want to use it without Auth0, it looks you'd only need to change the files in the ./utils dir, maybe even just |
@ugiacoman I've started implementing a small server with passwordless.net, let me know if you want to get my code as a starting point |
@impronunciable That'd be awesome! I was actually going to do something similar with Twitter Fabric's Digits. |
@impronuncible i suggest not using password less.net , instead you can just use passport-local, and just send users a link with their email and token in query string. |
Thanks @impronunciable ❤️ @ugiacoman yeah, it's pretty easy to remove the auth0 dependency. I used it because I didn't want to have a separate api to handle auth |
@jaredpalmer as far as I know, having #25 would be great but isn't blocking? I mean we have access to the server-side BTW considering |
The cookie approach can be very safe if done properly. Doing the following is fairly trivial:
|
There's also a very significant latency advantage when you can access authentication information directly on the server. |
Fixes #153 This is my attempt at #153 Following @rauchg instructions: - it uses an authentication helper across pages which returns a token if there's one - it has session synchronization across tabs - <strike>I deployed a passwordless backend on `now.sh` (https://with-cookie-api.now.sh, [src](https://github.com/j0lv3r4/next.js-with-cookies-api))</strike> The backend is included in the repository and you can deploy everything together by running `now` Also, from reviewing other PRs, I made sure to: - use [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch). - use [next-cookies](https://www.npmjs.com/package/next-cookies). Here's a little demo: ![GIF](https://i.imgur.com/067Ph56.gif)
@jaredpalmer you wrote We are 2 years later. Is there a server workflow to prevent loading js for protected pages? |
@lishine You have a ServerResponse in the |
Is there an example of auth with redux? |
You can try this example which is using redux, and check out if it works for you... |
I think this is more complicated problem when using Server Side API call results getInitialProps, because Virtual DOM uses old results after LOGOUT-LOGIN action. I'm thinking about solution |
EDITED
static async getInitialProps({ store, query: { rowsPerPage, pageIndex }, req, auth }) {
store.dispatch(TemporaryStoryActions.initPageState());
const isAuthenticated = () => req ? req.isAuthenticated()
: store.getState().auth.isAuthenticated;
if (isAuthenticated()) {
// fetch initial data
const TemporaryStoryApiProxy = withCookieProxy(req, TemporaryStoryApi);
await TemporaryStoryApiProxy.fetchTemporaryStories({
rowsPerPage: rowsPerPage || 15,
pageIndex: pageIndex || 0, }).then(json => {
store.dispatch(TemporaryStoryActions.loadTemporaryStories(
json.rowsPerPage, json.pageIndex, json.count, json.rows));
}).catch(error => {
if (error.response && error.response.status === 403) {
store.dispatch(AuthActions.initState(false, null));
return;
}
throw error;
});
}
if (!isAuthenticated()) {
// => if client side fetch failed with 403, isAuthenticated() turns off to false
// register logined action for client side login succeeded
const reloadAction = TemporaryStoryActions.fetchTemporaryStories({
rowsPerPage: rowsPerPage || 15,
pageIndex: pageIndex || 0,
});
store.dispatch(AuthActions.addLoginedAction(reloadAction));
}
return {
...store.getState(),
} } } export const withLogin = Page => class SecurePage extends React.Component {
static async getInitialProps (ctx) {
if (ctx.req && ctx.store) {
// server side
const isAuthenticated = ctx.req.isAuthenticated();
const { user } = ctx.req;
ctx.store.dispatch(AuthActions.initState(isAuthenticated, user));
}
return Page.getInitialProps && await Page.getInitialProps(ctx)
}
render () {
const { auth } = this.props;
return auth.isAuthenticated ? <Page {...this.props} /> : <LoginPage />
} } // when [front-end server] => [api server]
// on Server Side Rendering,
// needs to proxy Cookies which sent to Next.js page request
export const withCookieProxy = (req, targetApi) => {
if (!req) {
return targetApi;
}
targetApi.client.interceptors.request.use(config => {
const cookieString = Object.keys(req.cookies).map(key => `${key}=${req.cookies[key]}`).join('; ');
const headers = {
...(config.headers || {}),
Cookie: cookieString,
};
return {
...config,
headers: headers,
};
}, error => {
return Promise.reject(error);
});
return targetApi;
}; const loginEpic = (action$, state$) => action$.pipe(
ofType(AuthActionTypes.login),
mergeMap(action => {
const email = action.payload.email;
const password = action.payload.password;
return from(AuthApi.login(email, password))
.mergeMap(json => {
const user = json.user;
const loginedActions = state$.value.auth.loginedActions;
const successActions = [
AuthActions.removeAllLoginedActions(),
...loginedActions,
AuthActions.loginSuccess(user.id, user.name, user.last_login_date),
];
return from(successActions);
}).pipe(catchError(error => {
return of$(AuthActions.loginFail(error));
}));
})); |
Seems complicated when something simple would do, along the lines of :
|
This example was merged as part of Next.js 8 |
@timneutkens thanks for the link. looking at https://github.com/zeit/next.js/blob/canary/examples/with-cookie-auth/www/utils/auth.js#L26-L34 ... shouldn't there be some kind of check after Testing the example without a cookie leads to |
fix several typos mostly relating to subject verb agreement in how-to-use-cloudflare.js
I made an example here that has server-side pre-rendering + authentication w/ apollo |
Please keep in mind that OWASP security guidelines recommends against storing JWT token in Local Storage, i.e. "A single Cross Site Scripting can be used to steal all the data in these objects, so again it's recommended not to store sensitive information in local storage." Here's Auth0: Where to Store Tokens and Tom Abbott: Where to Store your JWTs – Cookies vs HTML5 Web Storage. Here is an example with Nuxt.js + Express.js proxy server + Django backend. Where Express server is used for proxying auth request to the actual backend and is handling CSRF protection when using JWT token storing in a cookie (imposes some restriction on the token's length / how much info can be stored in the JWT token): https://github.com/danjac/nuxt-python-secure-example |
@timneutkens I need some docs on how to send token from cookie 🍪 to SSR custom redux middleware. I am getting the cookies inside _app.js . But how should I pass it to customApimiddleware. Where I have written fetch requests. Thanks |
I think this is one of the best tutorials for handling authentication in a nextj.js app. I've seen things like storing tokens to localStorage (XSS), storing tokens in cookies (without handling CSRF), and even storing tokens in cookies from browser (both XSS and CSRF vulnerable). I really like your solution with the reverse proxy and sharing the session info between different services. I really would like to not create a custom server for next.js app, but I think it is the most straightforward way to both handling sessions and prevent csrf (and maybe adding the reverse proxy). I may even end up creating a monolith project (both for rendering the app and handling db operations etc). I've seen that some people (including ZEIT) keep the APIs stateless and let the next.js app to handle the session. It passes the tokens to APIs. But going with sessions only makes things a little bit more tight and less complicated. It would be a really better thing to have a full authentication example for next.js. With things like authentication for external APIs, keeping the session in next.js app, sharing session between services or passing tokens to them, and maybe even refreshing the tokens if they are expired. (A lot of people write a lot about JWTs and just use them in their tutorials, but they mostly don't even expire them or not even refresh them.) Anyway, you've written one of the most complete tutorials on this subject. So, thanks! I really hope there will be much more complete and clear examples and documentation. |
I, too, am at a loss as to which approach to choose. |
Worth checking out Auth0's new cookie based approach
Bear in mind they say this approach is "experimental" in the ReadMe |
This article is very helpful and it covers a lot of different architecures. Using API routes as proxy, login/logout via API routes, getting the token from API, setting it as HttpOnly cookie is a solid approach I think. |
@onderonur , thanks for auth0 article. |
I've used this method for one of my repos but it's still in draft, so make sure to test them yourself :) One other option is (if you want to have nearly the same flow as setting the token in local storage), you can use |
Appreciate this is an old thread (and a long running topic in general) but for those looking for additional references or examples, we've picked up work on NextAuth.js v2 recently. I mention it not so much as a plug – it's an open source project and a bunch of folks have helped out on it – but it's super simple to use and the code and approach might be useful as a reference to folks. For some background, like NextAuth v1, it uses cookies that are signed, prefixed and HTTP only, avoiding common security pitfalls of using client side tokens. NextAuth.js v2 supports signing in with Apple, Google, Facebook, Twitter, GitHub, Auth0, Okta, Slack, Discord and other OAuth providers (it supports both 1.x and 2.x). You can use it with MySQL, MariaDB, Postgres, MongoDB - or no database all (just OAuth and JSON Web Tokens for a 100% serverless solution). Usage is very simple, there is a Universal static method called import { useSession } from 'next-auth/client'
export default () => {
const [session, loading] = useSession()
return <>
{!loading && session && <p>Signed in as {session.user.name || session.user.email}.</p>}
{!loading && !session && <p><a href="/api/auth/signin">Sign in here</a></p>}
</>
} It is built for Next.js 9.x and Serverless, and doesn't have dependancies like Express or PassportJS. It includes an auth provider you can use in For more info, see the next-auth.js.org or check out the next-auth@beta on NPM
It's still a work in progress - we are still polishing up the documentation and event model - with a target release date of |
Great work this! |
This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you. |
With:
now.sh
I think this will be hugely helpful to a lot of newcomers.
The text was updated successfully, but these errors were encountered: