Finally I was able to make it work the way I wanted. Currently I have problems with Idp-initiated flow, I'll update the answer if I find the solution.
First configure your organization's Okta as Saml Sp for external Saml IdP. Api documentation is here. UI instructions are here. You should receive the information for SAML PROTOCOL SETTINGS
(IdP Issuer URI, IdP Single Sign-On URL and IdP Signature Certificate) from your external IdP.
Next step create Web application in Okta. Your Login redirect URIs
should finish with /authorization-code/callback
otherwise you will have to configure it in CallbackPath
property in the code (see below). You don't need to implement this endpoint, the framework does it for you.
Now install Okta.AspNetCore nuget and add this code in ConfigureServices
method
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OktaDefaults.MvcAuthenticationScheme;
})
.AddCookie()
.AddOktaMvc(new OktaMvcOptions
{
OktaDomain = "<okta_domain>",
ClientId = "<app_client_id>",
ClientSecret = "<app_client_secret>",
Scope = OktaDefaults.Scope,
});
You will find app_client_id
and app_client_secret
on the bottom of General tab of web application created in step 2.
Now the most interesting part. If you want to trigger authentication process for internal users (not external Saml Idp users) you call ChallengeAsync
method. Something like this:
[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
[HttpGet("login")]
public async Task Login()
{
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};
await HttpContext.ChallengeAsync(properties);
}
}
Whenever you hit /auth/login
url, you will be redirected to Okta for authentication.
When you want to trigger authentication process for an external Idp users you should add idp
parameter in AuthenticationProperties
class.
[ApiController]
[Route("[controller]")]
public class SsoController : ControllerBase
{
private readonly ILogger<SsoController> _logger;
private readonly IOktaClient _oktaClient;
public SsoController(ILogger<SsoController> logger, IOktaClient oktaClient)
{
_logger = logger;
_oktaClient = oktaClient;
}
[HttpGet]
public async Task Sso([FromQuery] string name)
{
var idpProviders = await _oktaClient.IdentityProviders.ListIdentityProviders(q: name, limit: 1).ToListAsync();
var idp = idpProviders.FirstOrDefault(p => p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
if (idp == null)
{
_logger.LogWarning($"idp name '{name}' doesn't exist");
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = "/"
};
await HttpContext.ForbidAsync(authenticationProperties);
}
else
{
var properties = new AuthenticationProperties
{
RedirectUri = "/",
Items = { ["idp"] = idp.Id }
};
await HttpContext.ChallengeAsync(properties);
}
}
}
When you hit /sso?name=MySamlIdp
url you will look for MySamlIdp
Identity Provider defined in step 1, and use its id to tell Okta where to redirect the user for authentication. The IOktaClient
defined in Okta.Sdk nuget.