How to provide frontend with JSON web token after server authentication?
Asked Answered
K

4

25

So far I have only dealt with server-rendered apps, where after a user logs in via username/password or using an OAuth provider (Facebook etc.), the server just sets a session cookie while redirecting to the relevant page.

However now I'm attempting to build an app using a more 'modern' approach, with React on the frontend and a JSON API backend. Apparently the standard choice for this is to use a JSON web token for authentication, however I'm having trouble working out how I'm meant to provide the JWT to the client so it can be stored in session/local storage or wherever.

Example to illustrate better:

  1. User clicks link (/auth/facebook) to log in via Facebook

  2. User is redirected and shown Facebook login form and/or permission dialog (if necessary)

  3. Facebook redirects user back to /auth/facebook/callback with an authorization code in tow, the server exchanges this for an access token and some information about the user

  4. Server finds or creates the user in the DB using the info, then creates a JWT containing a relevant subset of the user data (e.g. ID)

  5. ???

At this point I just want the user to be redirected to the main page for the React app (let's say /app) with the JWT in tow, so the frontend can take over. But I can't think of an (elegant) way to do that without losing the JWT along the way, other than to put it in the query string for the redirect (/app?authtoken=...) - but that will display in the address bar until I remove it manually using replaceState() or whatever, and seems a little weird to me.

Really I'm just wondering how this is typically done, and I'm almost sure I'm missing something here. The server is Node (Koa with Passport), if that helps.

Edit: To be clear, I'm asking what the best way is to provide a token to the client (so it can be saved) after an OAuth redirect flow using Passport.

Kirstinkirstyn answered 2/2, 2017 at 7:12 Comment(2)
Did you find out the answer? I'm trying find answer for that problem, but I can't find any useful information.Melquist
I'v been struggling with this issue also and what's the best practice?Clinker
C
21

I recently ran across this same issue, and, not finding a solution here or elsewhere, wrote this blog post with my in-depth thoughts.

TL;DR: I came up with 3 possible approaches to send the JWT to the client after OAuth logins/redirects:

  1. Save the JWT in a cookie, then extract it on the front-end or server in a future step (eg. extract it on the client with JS, or send a request to the server, server uses the cookie to get the JWT, returns the JWT).
  2. Send the JWT back as part of the query string (which you suggest in your question).
  3. Send back a server-rendered HTML page with a <script> tag that:
    1. Automatically saves the embedded JWT to localStorage
    2. Automatically redirects the client to whatever page you like after that.

(Since logging in with JWTs is essentially equivalent to "saving the JWT to localStorage, my favorite option was #3, but it's possible there are downsides I haven't considered. I'm interested in hearing what others think here.)

Hope that helps!

Constructionist answered 19/2, 2018 at 22:2 Comment(4)
Fantastic. Have been struggling between just the one and two options. Third one did not occur. Excellent work.Squawk
Thank you for answering this question properly! and writing a blog post about it! Extremely helpfulTrophic
Are these answers still valid or has there been another way of doing this discovered?Counterespionage
You can also send it in a custom header. Other than that i can't recommend to save your jwt to localstorage as it may be prone to xss.Sharla
C
4
  1. Client: Open a popup window via $auth.authenticate('provider name').
  2. Client: Sign in with that provider, if necessary, then authorize the application.
  3. Client: After successful authorization, the popup is redirected back to your app, e.g. http://localhost:3000, with the code (authorization code) query string parameter.
  4. Client: The code parameter is sent back to the parent window that opened the popup.
  5. Client: Parent window closes the popup and sends a POST request to /auth/provider withcode parameter.
  6. Server: Authorization code is exchanged for access token.
  7. Server: User information is retrived using the access token from Step 6.
  8. Server: Look up the user by their unique Provider ID. If user already exists, grab the existing user, otherwise create a new user account.
  9. Server: In both cases of Step 8, create a JSON Web Token and send it back to the client.
  10. Client: Parse the token and save it to Local Storage for subsequent use after page reload.

    Log out

  11. Client: Remove token from Local Storage
Cist answered 26/12, 2017 at 4:42 Comment(1)
How is step 4 done with code? I know we can access parent window doing window.opener, but where do we go from there?Overvalue
P
0

here is a login request from the server side. it's storing the token in the header:

router.post('/api/users/login', function (req, res) {
  var body = _.pick(req.body, 'username', 'password');
  var userInfo;

models.User.authenticate(body).then(function (user) {
      var token = user.generateToken('authentication');
      userInfo = user;

      return models.Token.create({
        token: token
      });
    }).then(function (tokenInstance) {
      res.header('Auth', tokenInstance.get('token')).json(userInfo.toPublicJSON());
    }).catch(function () {
      res.status(401).send();
    });
});

here is the login request on the react side, where I am grabbing the token from the header and setting the token in local storage once the username and password pass authentication:

handleNewData (creds) {
    const { authenticated } = this.state;
    const loginUser = {
        username: creds.username,
        password: creds.password
    }
    fetch('/api/users/login', {
        method: 'post',
        body: JSON.stringify(loginUser),
        headers: {
            'Authorization': 'Basic'+btoa('username:password'),
            'content-type': 'application/json',
            'accept': 'application/json'
        },
        credentials: 'include'
    }).then((response) => {
        if (response.statusText === "OK"){
            localStorage.setItem('token', response.headers.get('Auth'));
            browserHistory.push('route');
            response.json();
        } else {
            alert ('Incorrect Login Credentials');
        }
    })
}
Plotkin answered 16/2, 2017 at 5:31 Comment(1)
This does not address OP's question, this is an auth strategy for local auth, not social provider oauth login flow with redirect back to app after successful auth. In OP's case, there is no login post request being made which can send a response back to the frontend, there is just a redirect on successfully completing auth flow with a 3rd party providerTrophic
H
-2

When you get a token from any passport authentication sites you have to save the token in your browser's localStorage. The Dispatch is Redux's Middleware. Ignore dispatch if you don't use redux in your app. you can just use setState here (A bit weird without redux).

Client-side:

Here's something similar API of mine, which returns token.

saving tokens

axios.post(`${ROOT_URL}/api/signin`, { email, password })
        .then(response => {

            dispatch({ type: AUTH_USER }); //setting state (Redux's Style)
            localStorage.setItem('token', response.data.token); //saving token
            browserHistory.push('/home'); //pushes back the user after storing token
        })
        .catch(error => {
            var ERROR_DATA;
            try{
                ERROR_DATA = JSON.parse(error.response.request.response).error;
            }
            catch(error) {
                ERROR_DATA = 'SOMETHING WENT WRONG';
            }
            dispatch(authError(ERROR_DATA)); //throw error (Redux's Style)
        });

So When you make some authenticated requests,you have to attach the token with the request in this form.

authenticated requests

axios.get(`${ROOT_URL}/api/blog/${blogId}`, {
        headers: { authorization: localStorage.getItem('token') } 
//take the token from localStorage and put it on headers ('authorization is my own header')
    })
        .then(response => {
            dispatch({
                type: FETCH_BLOG,
                payload: response.data
            });
        })
        .catch(error => {
            console.log(error);
        });

Here's my index.js: The token is checked each and everytime, so even if the browser got refreshed, you can still set the state.

checks if the user is authenticated

const token = localStorage.getItem('token');

if (token) {
   store.dispatch({ type: AUTH_USER })
}

ReactDOM.render(
  <Provider store={store}>
    <Router history={browserHistory}>
      <Route path="/" component={App}> 
..
..
..
      <Route path="/blog/:blogid" component={RequireAuth(Blog)} />
//ignore this requireAuth - that's another component, checks if a user is authenticated. if not pushes to the index route
      </Route>
    </Router>
  </Provider>
  , document.querySelector('.container'));

All that dispach actions does is it sets the state.

my reducer file(Redux only) else you can just use setState() in your index route file to provide the state to the whole application. Every time the dispatch is called, it runs a similar reducer file like this which sets the state.

setting the state

import { AUTH_USER, UNAUTH_USER, AUTH_ERROR } from '../actions/types';

export default function(state = {}, action) {
    switch(action.type) {
        case AUTH_USER:
            return { ...state, error: '', authenticated: true };
        case UNAUTH_USER:
            return { ...state, error: '', authenticated: false };
        case AUTH_ERROR:
            return { ...state, error: action.payload };
    }

    return state;
} //you can skip this and use setState() in your index route instead

Delete the token from your localStorage to logout.

caution: Use any different name rather than token to save the token in your browser's localStorage

Server-Side:

considering your passport services file. You must set the header search. Here's passport.js

const passport = require('passport');
const ExtractJwt = require('passport-jwt').ExtractJwt;
const JwtStrategy = require('passport-jwt').Strategy;
..
.. 
..
..
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'), //client's side must specify this header
secretOrKey: config.secret
};

const JWTVerify = new JwtStrategy(jwtOptions, (payload, done) => {
    User.findById(payload._id, (err, user) => {
        if (err) { done(err, null); }

        if (user) {
            done(null, user);
        } else {
            done(null, false);
        }
    });
});

passport.use(JWTVerify);

In my router.js

const passportService = require('./services/passport');
const requireAuthentication = passport.authenticate('jwt', { session: false });
..
..
..
//for example the api router the above react action used
app.get('/api/blog/:blogId', requireAuthentication, BlogController.getBlog);
Hypognathous answered 2/2, 2017 at 8:17 Comment(3)
I understand how to use a token to authenticate from the frontend, I'm only interested in how to get it to the client in the first place. In saving tokens you make an Ajax request from the frontend to sign in with username and password, but when using an OAuth strategy like passport-facebook (or passport-twitter, etc.) that isn't an option because of the redirect flow. Unless I'm missing something.Kirstinkirstyn
Does not address the original question of how to send authentication credentials FROM the server to the frontend.Fiorin
This is the flow for passport-local auth strategy, OP's issue is specifically with passport social oauth strategies, in which a user is logged in by being redirected to a social provider's auth flow, and then redirected back to the app post authentication. There is no post request involved in OP's situation, their issue is how to pass the token back to frontend after user is redirected to the callback routeTrophic

© 2022 - 2024 — McMap. All rights reserved.