loading...

OIDC Authentication with React & Identity Server 4

tappyy profile image Andy ・4 min read

I put this small demo together with the following objectives:

  • Authenticate a React app user via Identity Server 4 using OIDC.
  • Store authenticated user details in a central store client side.
  • Have a public and a protected route within the app. Only authenticated users can access protected route.
  • Fetch data from a protected web API using a JWT. Only authenticated users can access the API.

Basic Architecture

  • React app will serve as the customer facing site.
  • Identity Server 4 will implement OpenID Connect and be used to authenticate users.
  • .NET Core API will have a protected enpoint that will serve some doughnut-y goodness 🍩.

Identity Server πŸ€–

Starting with one of the .NET templates provided by Identity Server, we need to configure our client, API resource and test user. For the purpose of this demo I will just create a single client, API resource and test user: Peter Parker πŸ•·οΈ.

The GetClients function of config.cs is configured as follows:

public static IEnumerable<Client> GetClients()
{
    return new[]
    {
        new Client
        {
            // unique ID for this client
            ClientId = "wewantdoughnuts", 
            // human-friendly name displayed in IS
            ClientName = "We Want Doughnuts", 
            // URL of client
            ClientUri = "http://localhost:3000", 
            // how client will interact with our identity server (Implicit is basic flow for web apps)
            AllowedGrantTypes = GrantTypes.Implicit, 
            // don't require client to send secret to token endpoint
            RequireClientSecret = false, 
            RedirectUris =
            {             
                // can redirect here after login                     
                "http://localhost:3000/signin-oidc",            
            },
            // can redirect here after logout
            PostLogoutRedirectUris = { "http://localhost:3000/signout-oidc" }, 
            // builds CORS policy for javascript clients
            AllowedCorsOrigins = { "http://localhost:3000" }, 
            // what resources this client can access
            AllowedScopes = { "openid", "profile", "doughnutapi" }, 
            // client is allowed to receive tokens via browser
            AllowAccessTokensViaBrowser = true 
        }
    };
}

Also in config.cs, we can add our web API as a resource in GetApis:

public static IEnumerable<ApiResource> GetApis()
{
    return new ApiResource[]
    {
        // name and human-friendly name of our API
        new ApiResource("doughnutapi", "Doughnut API") 
    };
}

Web API πŸ•ΈοΈ

Our web API will serve up doughnut freshness from behind a protected endpoint. When calling the API from our React app, we will pass a bearer token in the request headers. The API can verify the token and give us what we want.

In the .NET Core Web API template project we can add bearer token authentication by adding the following to the ConfigureServices method in Startup.cs:

services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        // URL of our identity server
        options.Authority = "https://localhost:5001";
        // HTTPS required for the authority (defaults to true but disabled for development).
        options.RequireHttpsMetadata = false; 
        // the name of this API - note: matches the API resource name configured above
        options.Audience = "doughnutapi"; 
    });

Next, we can add the middleware to the app by adding app.UseAuthentication() to the Configure method of Startup.cs. This allows authentication to be performed on every request.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseCors(builder =>
          builder
            .WithOrigins("http://localhost:3000")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials()
        );

    app.UseAuthentication();
    app.UseMvc();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Doughnut API is running!");
    });
}

React SPA πŸ‘Ύ

We can use Create React App to spin up a quick React project. From there, we can add our components and services for authenticating a user.

userService.js

We are using IdentityModel's oidc-client to implement our OIDC flow in React. I've created a userService that will abstract all functionality relating to OIDC and user management. oidc-client exposes a UserManager class that requires a config object:

const config = {
  // the URL of our identity server
  authority: "https://localhost:5001", 
  // this ID maps to the client ID in the identity client configuration
  client_id: "wewantdoughnuts", 
  // URL to redirect to after login
  redirect_uri: "http://localhost:3000/signin-oidc", 
  response_type: "id_token token",
  // the scopes or resources we would like access to
  scope: "openid profile doughnutapi", 
  // URL to redirect to after logout
  post_logout_redirect_uri: "http://localhost:3000/signout-oidc", 
};

// initialise!
const userManager = new UserManager(config)

userService.js exports various functions that use the userManager class created above.

Initiating OIDC flow

Using userService.signinRedirect(), we can initiate the OIDC login flow. This will redirect the user to the login screen of Identity Server, and once authenticated, will redirect them back to the redirect_uri provided when configuring the UserManager class.

Callback Routes

For the simplicity of this demo, 2 callback routes have been configured: /signin-oidc and /signout-oidc.

Once the user has logged in, they are redirected to /signin-oidc on the client. On page load, userService.signinRedirectCallback() will process the response from the OIDC authentication process. Once complete, the user is redirected to the home page and authentication has been successful! Yay!

function SigninOidc() {
  const history = useHistory()
  useEffect(() => {
    async function signinAsync() {
      await signinRedirectCallback()
      // redirect user to home page
      history.push('/')
    }
    signinAsync()
  }, [history])

Similarly, when the user logs out they are redirected to Identity Server to confirm logout then back to /signout-oidc on the client. This is where we can do any further actions such as redirecting the user to a 'Logout Successful!' page.

AuthProvider

Inspired by this Medium article on implementing OIDC in React, I used React Context to create an AuthProvider to wrap all components of the app. I'm only using this to handle events triggered in our userManager class:

userManager.current.events.addUserLoaded(onUserLoaded)
userManager.current.events.addUserUnloaded(onUserUnloaded)
userManager.current.events.addAccessTokenExpiring(onAccessTokenExpiring)
userManager.current.events.addAccessTokenExpired(onAccessTokenExpired)
userManager.current.events.addUserSignedOut(onUserSignedOut)

The UserLoaded event is used to store the user object from Identity Server in Redux. This user object includes an access token which is added to the authorization header in axios.

We're done! πŸ‘Š

A user of our React app can successfully authenticate via Identity Server and call our web API to get some doughnut-y goodness!

React app home page

This doesn't implement more advanced features such as silently renewing tokens but it does serve as a demonstration of adding OIDC to a React app.

Feel free to clone the Github repo and have a poke around the demo and source code. PRs also welcome!

Further Reading πŸ“–

Some useful links that are related to this post πŸ˜ƒ

Posted on by:

tappyy profile

Andy

@tappyy

Your friendly neighbourhood software engineer.

Discussion

pic
Editor guide