DEV Community

Andy
Andy

Posted on

OIDC Authentication with React & Identity Server 4

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 
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

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") 
    };
}
Enter fullscreen mode Exit fullscreen mode

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"; 
    });
Enter fullscreen mode Exit fullscreen mode

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!");
    });
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 😃

Latest comments (14)

Collapse
 
maulikgajera profile image
Maulik Gajera • Edited

Hello, I have used your article, project runs and authenticate in my local system, but when i deployed it to IIS server with domain configurations it gave me error of 'un_authorized client'. Can you please help with it?

Thanks.

Collapse
 
cubikca profile image
Brian Richardson

Thanks for the primers on Identity Server. Was getting a bit lost.

Collapse
 
hari9526 profile image
Harikrishnan R • Edited

I am not able to login. When I type 'alice' for username and password, nothing happens and I get an error in the console like this

Refused to connect to 'ws://localhost:50071/IdentityServer/' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

Is there something I am missing?

Collapse
 
nonbob profile image
nonbob • Edited

In my experience, you need to disable hot reload. There are a number of places you can disable this, but for me the critical one was in the Launch Profiles dialog in Visual Studio. You get to it from the Debug section of a project's properties (at least in VS 2022, which is what I'm using).

dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
jontycool profile image
Tathagat Mohanty

Any help for adding Silent Renewal to this project ? Because after 20-30 mins I am getting idp claim missing error

Collapse
 
sfmb profile image
Fraser

Did you get any further with this issue?

Collapse
 
wapenshaw profile image
Siddharth Abbineni

@tappyy Hey, Now that you have a local identity server you are able to manage users and roles with ASP NET Identity and directly query the API. However in cases where one has to use a 3rd Party OIDC/Identity Provider, how would you store user data in your API database for referential purposes?

Collapse
 
programmvilli profile image
programmvilli

Thank you for this article. Will hopefully work for my project :)

Collapse
 
jinyanghuang profile image
Jinyang (Michael) Huang

Hi, I followed you code and everything works fine. However, if I enter a sub-page and refresh the page, it goes blank. Do you know why and how to fix that?

Collapse
 
jinyanghuang profile image
Jinyang (Michael) Huang

I noticed that it is due to the user info being reset every time. So it is redirected to login page and then main page. Is there a way to solve this?

Collapse
 
anjkr profile image
anj-kr

Are you able to resolve this ? I'm facing the same issue when reloading the application, it is triggering the flow again. Please let me know

Thanks - Dan

Collapse
 
tappyy profile image
Andy

You could persist the user data in local storage or use something like redux-persist to manage the state rehydration for you :)

Collapse
 
carduscarv profile image
Cardus Crvil • Edited

hi, what great article! this is help me a lot to solve my sso implement problem.

i can login to my apps now, but i have an issue on the console.. it gave me 'consent_required' error till i got 'user signed out'. I love to ask if you know how to solve it..

thank you!

Collapse
 
tappyy profile image
Andy

Hey Cardus,

I set "requireConsent" to false in the Identity config to bypass the consent page. Have you done this too?