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!
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 😃
Top comments (14)
@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?
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!
Hey Cardus,
I set "requireConsent" to false in the Identity config to bypass the consent page. Have you done this too?
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?
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...
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?
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?
You could persist the user data in local storage or use something like redux-persist to manage the state rehydration for you :)
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
Thank you for this article. Will hopefully work for my project :)
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.
Any help for adding Silent Renewal to this project ? Because after 20-30 mins I am getting idp claim missing error
Did you get any further with this issue?
Thanks for the primers on Identity Server. Was getting a bit lost.