Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/integrate-sso-asp-net-core-vie/
This is the second part of a three-part series about single sign-on (SSO) using SAML with OKTA. You can find the other parts from here.
We will use the application we registered in the first part to authenticate users before they can access confidential information on our ASP.NET Core web app. See this link for how to retrieve the Identity Provider Issuer and Identity Provider Metadata URL.
You can download the sample application in this article from the link below.
https://github.com/duongntbk/SamlSample
Overview of the authentication process
We will add OKTA integration as a new authentication service to our web app. The whole process will be cookie based, and the flow is below.
- Users try to access our app.
- Our app checks users’ cookie, if they are not logged in, they are redirected to our local login page, which is
/account/login
in our sample app. - Our login page forwards users to the OKTA login page with an authentication request, and they need to login with OKTA.
- OKTA verifies users’ credentials, if the credentials are incorrect, or users do not have permission to view our app, they will see an error page on OKTA.
- Otherwise, users are redirected back to our web app with a signed authentication response.
- Our login page checks that response to verify that it is indeed from OKTA and hasn’t expired yet.
- If all verifications pass, users’ cookie will be updated then they are redirected back to the page they first tried to access.
- Now users can access our web app.
Integrate OKTA with an existing web app
As all things security related, we won’t roll our own authentication code and will use a library called Sustainsys instead. Install it by running the following command.
dotnet add package Sustainsys.Saml2 --version 2.8.0
We will register a new authentication service inside ConfigureServices of our Startup.cs file.
services
.AddAuthentication("OktaSaml")
.AddSamlAuthentication(Configuration.Saml);
We also need to add authentication and authorization middleware to our web app.
app
.UseAuthentication()
.UseAuthorization();
Here, we name our authentication service OktaSaml and configure it by calling an extension method, which in turn calls another extension method to pass in all configuration options. Let’s take a look at both of them. In our sample, the app is hosted at https://localhost:5001
.
The AuthenticationExtensions class
public static AuthenticationBuilder AddSamlAuthentication(this AuthenticationBuilder auth,
Action<Saml2Options> configureOptions) =>
auth
.AddCookie("OktaSaml", options => {
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.Path = "/";
})
.AddSaml2(configureOptions);
This method sets cookie option for our authentication service and then register it with a schema name, which is OktaSaml in our case.
By default, users will be redirected to /account/login
before being forwarded to OKTA. But we can change it to a different URL by adding this line.
options.LoginPath = "<new local login path>"
The ConfigurationExtensions class
public static void Saml(this IConfiguration config, Saml2Options options)
{
options.SPOptions.PublicOrigin = "https://localhost:5001/";
options.SPOptions.EntityId = "https://localhost:5001/Saml2";
options.SPOptions.ReturnUrl = new Uri("/", UriKind.Relative);
var idp = new IdentityProvider(new EntityId("<Identity Provider Issuer from OKTA>"), options.SPOptions)
{
MetadataLocation = "<Identity Provider Metadata URL from OKTA>",
AllowUnsolicitedAuthnResponse = true
};
options.IdentityProviders.Add(idp);
}
We will go through each line and see what they do.
options.SPOptions.PublicOrigin = "https://localhost:5001/";
The PublicOrigin value here will be used as the root path to generate the URL of the endpoint which verifies OKTA response. This endpoint is a special page created automatically by Sustainsys at /Saml2/Acs
. With our PublicOrigin, its full path becames https://localhost:5001/Saml2/Acs
. Most of the time, we can just use our application’s root path here.
options.SPOptions.EntityId = "https://localhost:5001/Saml2";
This is the Id that our application uses when it sends authentication requests to OKTA. It must match the value of Audience URI (SP Entity ID) which we set in our OKTA application.
options.SPOptions.ReturnUrl = new Uri("/", UriKind.Relative);
This is the default return URL, the user will be redirected there after they are logged in if there is no return url in the authentication request.
var idp = new IdentityProvider(new EntityId("<Identity Provider Issuer from OKTA>"), options.SPOptions)
{
MetadataLocation = "<Identity Provider Metadata URL from OKTA>",
AllowUnsolicitedAuthnResponse = true
};
This is where we tell our application which OKTA endpoint to use for authentication. AllowUnsolicitedAuthnResponse means that OKTA can send authentication responses to our application without our application initiating the login process. This option is used if you want the user to be able to login by clicking a button on OKTA site.
Our local login page
Unlike a normal web app, our login page won’t do anything to check user credentials, instead it sends authentication requests to OKTA then waits and verifies OKTA’s authentication responses.
// route: /account/login
public IActionResult OnGetLogin(string returnUrl)
{
var redirectUri = Url.Page("account", "callback", new { returnUrl });
return new ChallengeResult("Saml2",
new AuthenticationProperties() { IsPersistent = true, RedirectUri = redirectUri });
}
// route: /account/callback
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl)
{
var auth = await HttpContext.AuthenticateAsync();
if (!auth.Succeeded)
{
return Unauthorized();
}
await HttpContext.SignInAsync(auth.Principal, auth.Properties);
return LocalRedirect(returnUrl);
}
You might think that OKTA will send authentication responses directly to /account/callback
, but actually the response is sent to /Saml2/Acs
which we mentioned above. /Saml2/Acs
will verify that OKTA’s response is valid before returning it to /account/callback
for further processing. This explains why we set Single sign on URL for our app on OKTA to https://localhost:5001/Saml2/Acs in part one.
Demo
First, we need to update our appsettings.json
file with details of our identity provider.
"Authentication": {
"Saml": {
"EntityId": "https://localhost:5001/Saml2",
"IdentityProviderIssuer": "<identity provider entity id>",
"MetadataUrl": "<link to metadata file on identity provider>"
}
}
Our sample app has 3 different pages, Home, Public and Secret. We mark Secret for authentication by adding this attribute to it.
[Authorize(AuthenticationSchemes = "OktaSaml")]
Users can access Home and Public without logging in.
But they will be redirected to OKTA’s login page if they try to access Secret.
If they input wrong credentials, OKTA will display an error.
Even when they input correct credentials, if they don’t have permission to access the app on OKTA, they will still see an error.
If they input correct credentials AND have correct permission, they can access Secret page.
As mentioned earlier, it’s also possible to login to our app directly from OKTA. From users’ homepage on OKTA, they can view all their assigned apps. By clicking on an app, OKTA will send an unsolicited authentication response to our web app. And if our app is configured with AllowUnsolicitedAuthnResponse
option, it will allow users to login.
Conclusion
We managed to add SSO with OKTA into our sample app. But what if we want to test our integration in a controlled environment without depending on an outside service? Find out here in part three.
Hi! great tutorial, could You only tell me where exactly this response from okta after login is store? it should be in our web app? or better in browser?
Hi Mat,
Okta’s reponse is not stored any where. It is processed server side to verify that users are logged in. If they are, their browser cookies will be updated with a token.
Hi ! Thanks for response,
I was wondering if it is store in browser, now I know, thanks a lot !
Thank you so much! Your explanation & instructions were super helpful!
You are welcome!
Please check out my other posts as well.
thank you!, but where are the Controllers for Home, Public and Secret ?
I used Razor Page, so there is no Controller class. You can find all the page models inside the
Pages
folder of my repository.Please see this link:
https://github.com/duongntbk/SamlSample/tree/master/SamlSample/Pages
For example, this is the page model for
Public
:https://github.com/duongntbk/SamlSample/blob/master/SamlSample/Pages/Public.cshtml.cs
I downloaded the samlsample project and followed instructions. The site runs but when I click “Secret”, instead of going to okta login page, it throws an 404 error. I believe I was following your instructions exactly the same. Any suggestions?
It seems that the web app cannot load the metadata from the provided URL. Please open
appsettings.json
and verify that you can access the path inAuthentication.Saml.MetadataUrl
.You can try opening it from your browser and see if the browser can download a metadata file.
Sorry for late reply. You are right, my MetadataUrl was wrong before, but after I changed to correct one, I still got the same 404 Not found error. Any other suggestions?
It’s working now. Thanks
I’m glad that you figure it out. Maybe you can post your problem here in case someone else has the same issue?
Hi, I’ve followed your guide here and looked at the sample code and set it up as exactly as I can with my own application. When I click on my protected page, Okta loads up, I login, and then I get directed to https://localhost:5001/Saml2/Acs with a 404 error:
This localhost page can’t be found
No webpage was found for the web address: https://localhost:5001/Saml2/Acs
I’ve checked the logs on the application in Okta and it’s saying there was a successful login. In developer console I can see the response from Okta, but then I hit that 404 error above. Any advice please?
Hi Paul,/Saml2/Acs (case sensitive).
Please check if your root path is indeed `https://localhost:5001`. If it is not, you need to update the Single Sign On URL in Okta to
Also, I guess you have download my sample code? Did you try hooking it up with your Okta server and see if it works?
Thanks for this guide. Could you tell me why my IDE might not be picking up .AddSamlAuthentication in the second step?
Hi Jake,
AddSamlAuthentication
is an extension method, which the IDE sometime can’t pick up.In that case, please add this statement to your class.