19
votes

Lately I'm trying to set-up authentication using IdentityServer4 with a React client. I followed the Adding a JavaScript client tutorial (partly) of the IdentityServer documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf also using the Quickstart7_JavaScriptClient file.

The downside is that I'm using React as my front-end and my knowledge of React is not good enough to implement the same functionality used in the tutorial using React.

Nevertheless, I start reading up and tried to get started with it anyway. My IdentityServer project and API are set-up and seem to be working correctly (also tested with other clients).

I started by adding the oidc-client.js to my Visual Code project. Next I created a page which get's rendered at the start (named it Authentication.js) and this is the place where the Login, Call API and Logout buttons are included. This page (Authentication.js) looks as follows:

import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';

export default class Authentication extends Component {
    constructor(props) {
      super(props);
    }

    render() {
      return (
        <div>
            <div>  
                <button id="login" onClick={() => {login()}}>Login</button>
                <button id="api" onClick={() => {api()}}>Call API</button>
                <button id="logout" onClick={() => {logout()}}>Logout</button>

                <pre id="results"></pre>

            </div>  

            <div>
                <Route exact path="/callback" render={() => {window.location.href="callback.html"}} />

                {/* {<Route path='/callback' component={callback}>callback</Route>} */}
            </div>
        </div>
      );
    }
  }

In the testoidc.js file (which get's imported above) I added all the oidc functions which are used (app.js in the example projects). The route part should make the callback.html available, I have left that file as is (which is probably wrong).

The testoidc.js file contains the functions as follow:

import Oidc from 'oidc-client'


export function log() {
  document.getElementById('results').innerText = '';

  Array.prototype.forEach.call(arguments, function (msg) {
      if (msg instanceof Error) {
          msg = "Error: " + msg.message;
      }
      else if (typeof msg !== 'string') {
          msg = JSON.stringify(msg, null, 2);
      }
      document.getElementById('results').innerHTML += msg + '\r\n';
  });
}

var config = {
  authority: "http://localhost:5000",
  client_id: "js",
  redirect_uri: "http://localhost:3000/callback.html",
  response_type: "id_token token",
  scope:"openid profile api1",
  post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);

mgr.getUser().then(function (user) {
  if (user) {
      log("User logged in", user.profile);
  }
  else {
      log("User not logged in");
  }
});

export function login() {
  mgr.signinRedirect();
}

export function api() {
  mgr.getUser().then(function (user) {
      var url = "http://localhost:5001/identity";

      var xhr = new XMLHttpRequest();
      xhr.open("GET", url);
      xhr.onload = function () {
          log(xhr.status, JSON.parse(xhr.responseText));
      }
      xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
      xhr.send();
  });
}

export function logout() {
  mgr.signoutRedirect();
} 

There are multiple things going wrong. When I click the login button, I get redirected to the login page of the identityServer (which is good). When I log in with valid credentials I'm getting redirected to my React app: http://localhost:3000/callback.html#id_token=Token

This client in the Identity project is defined as follows:

new Client
                {
                    ClientId = "js",
                    ClientName = "JavaScript Client",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    AllowAccessTokensViaBrowser = true,

                    // where to redirect to after login
                    RedirectUris = { "http://localhost:3000/callback.html" },

                    // where to redirect to after logout
                    PostLogoutRedirectUris = { "http://localhost:3000/index.html" },

                    AllowedCorsOrigins = { "http://localhost:3000" },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "api1"
                    }

                }

Though, it seems the callback function is never called, it just stays on the callback url with a very long token behind it..

Also the getUser function keeps displaying 'User not logged in' after logging in and the Call API button keeps saying that there is no token. So obviously things are not working correctly. I just don't know on which points it goes wrong. When inspecting I can see there is a token generated in the local storage:

enter image description here

Also when I click the logout button, I get redirected to the logout page of the Identity Host, but when I click logout there I don't get redirected to my client.

My questions are:

  • Am I on the right track implementing the oidc-client in combination with IdentityServer4?
  • Am I using the correct libraries or does react require different libraries than the oidc-client.js one.
  • Is there any tutorial where a react front-end is used in combination with IdentityServer4 and the oidc-client (without redux), I couldn't find any.
  • How / where to add the callback.html, should it be rewritten?

Could someone point me in the right direction, there are most likely more things going wrong here but at the moment I am just stuck in where to even begin.

1
Hi Nicolas, it'd help if you can put a minimal repro code somewhere so that it's easy for folks to debug and find out what's broken. I have successfully integrated ID4 with React. You don't need a separate callback.html, all of this can be handled easily using browser routes in a single page application. But having access to your code will help get to the bottom of this quicker.hazardous
@hazardous Thank you for your reply and sorry for my late reaction. Due to vacation and all it was a bit hectic. My knowledge about React and ID4 has fortunately increased since I posted this, In the coming weeks I will try to implement it again. Should I fail then I'll try upload a repro as soon as possible. May I succeed then I will post the results back here as well.Nicolas
What is your back-end for this project? I ask because often times there's no reason to implement the OIDC flow completely in the front-end, and it's trivial to implement the whole flow outside of react. For example, I've done many react projects where my entire OIDC functionality is handled by the .net OIDC middleware, and it's often the better way to go.Daniel Tabuenca

1 Answers

7
votes

IdentityServer4 is just a backend implementation of OIDC; so, all you need to do is implement the flow in the client using the given APIs. I don't know what oidc-client.js file is but it is most likely doing the same thing that you could have implemented yourself. The flow itself is very simple:

  1. React app prepares the request and redirects the user to the Auth server with client_id and redirect_uri (and state, nonce)
  2. IdentityServer checks if the client_id and redirect_uri match.
    • If the user is not logged in, show a login box
    • If a consent form is necessary (similar to when you login via Facebook/Google in some apps), show the necessary interactions
    • If user is authenticated and authorized, redirect the page to the redirect_uri with new parameters. In your case, you the URL will look like this: https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
  3. Now, the React app needs to parse the URL, access the values, and store the token somewhere to be used in future requests:

Easiest way to achieve the logic is to first set a route in the router that resolves into a component that will do the logic. This component can be "invisible." It doesn't even need to render anything. You can set the route like this:

<Route path="/cb" component={AuthorizeCallback} />

Then, implement OIDC client logic in AuthorizeCallback component. In the component, you just need to parse the URL. You can use location.hash to access #access_token=...&id_token=...&stuff-like-nonce-and-state part of the URL. You can use URLSearchParams or a 3rd party library like qs. Then, just store the value in somewhere (sessionStorage, localStorage, and if possible, cookies). Anything else you do is just implementation details. For example, in one of my apps, in order to remember the active page that user was on in the app, I store the value in sessionStorage and then use the value from that storage in AuthorizeCallback to redirect the user to the proper page. So, Auth server redirects to "/cb" that resolves to AuthorizeCallback and this component redirects to the desired location (or "/" if no location was set) based on where the user is.

Also, remember that if the Authorization server's session cookie is not expired, you will not need to relogin if the token is expired or deleted. This is useful if the token is expired but it can be problematic when you log out. That's why when you log out, you need to send a request to Authorization server to delete / expire the token immediately before deleting the token from your storage.