I'm trying to implement openid with okta using the okta widget. The documentation can be found here:
https://developer.okta.com/code/javascript/okta_sign-in_widget
I've got the widget rendering and I've modified the AccountController Login Method to return a Challenge Response.
if (!HttpContext.User.Identity.IsAuthenticated) { var properties = new AuthenticationProperties(); properties.Items.Add("sessionToken", sessionToken); properties.RedirectUri = "/app/dashboard"; return Challenge(properties, OktaDefaults.MvcAuthenticationScheme); } return RedirectToAction("Index", "Home");
I did this based on the okta example at https://github.com/oktadeveloper/okta-aspnetcore-mvc-example.
The okta sample works. But with my aspnetzero test project, I get a cors error after the challenge response.
Failed to load https://dev-586182.oktapreview.com/oauth2/default/v1/authorize?client_id=0oagaur4ukBFH91Od0h7&redirect_uri=http%3A%2F%2Flocalhost%3A62114%2Fauthorization-code%2Fcallback&response_type=code&scope=openid%20profile%20email&response_mode=form_post&nonce=636749499861104637.OWVjOGM2MzItODVhNS00MWU5LWI2NDQtZmE3YWEzMGMwZjIyZmQ2ODQwNTAtYjNiOC00ZTkwLThlYWYtOWFlNGRjOGE3YmIw&sessionToken=20111HIi49zni2fMgl9HsggpGhLnukMlTvIK2gVVWL_3c6bK7Ijq0e3&state=CfDJ8LRmRAoWNcxFrJRw5HHQysQzWhO-9Kwx2z8FNwUZylHUEde9SLy_fcsk3YAUaFtO4Maw_FxyHaTnpyc-HbXgmhjZwXgD5J8krSwBJWR83XmqGngBVCQqfboIKo5SmrFjv8g-tKqPFpwRKuSlD6yEWU2Q8lflrLrM5J5OAQep-beiBQqqaUThPKFdAw3v2w9MfRs95rXE4QUHFLrnw3L2SqLCzESVXpa3xiL_NwyhgsG7l69Anb-kTONGrtCREhbAljxfdPRosd1H0BQWbWsgoSlPeDwBDAdldkMntzJyTYtRtUw31jJRyKZCBlY_xkOXGqQNHIbJbqbCKGjgD3lh2B1saAbjvFwKoNxfi-CJxxBeoEEqoO24BFAGRN4Yl095VUeXHBEu0uCJ1RHyn3a4VsAOO0TsmeE1nqTEZuKHSpks&x-client-SKU=ID_NET451&x-client-ver=5.2.0.0: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:62114' is therefore not allowed access.
When I look at Fiddler to see what the differences are between the two, I see the following:
Okta Sample:
GET https://dev\-586182\.oktapreview\.com/oauth2/default/v1/authorize?client\_id=0oagavcxawHIwhL0v0h7&redirect\_uri=http%3A%2F%2Flocalhost%3A50336%2Fauthorization\-code%2Fcallback&response\_type=code&scope=openid%20profile%20email&response\_mode=form\_post&nonce=636749485315680253\.YWI5NmM0OTYtNmI5ZS00ZDdhLWI3YmQtZGRlODEyZDhlYTg1MmI3MWI0NDAtMzY5Ny00MmRkLTk0NTItN2NjNGM3MDk3OWMy&sessionToken=20111TZBaJj2UhRVDGyRC1ibEgwfPHi7BpH1FtjrqQdWKgQQcWeQAa5&state=CfDJ8LRmRAoWNcxFrJRw5HHQysT\_mCxdITM8LSpdFcyQylyYlrvf\_IULJTU1bxCNDrA4fwJTYirtf3BbBfx4hqb6R3nfp6aZLY6LltGFrllXW8AxP8oGhPSLZt\_dphp9MLFgnEiN2YbJwfoBfsclWolORc5l7o673dMOtOZm430zzDVQNybrio5Xl4e9NZKOCQ2UGvVR3T5T7NwbNG9jLbV\-Wl15qeh6tDZJvtv6yC8glj9SnIqMwDpa\-mGRE02NUG5UN8omJHDkqKu8xE8Da3P8lbeIZjTYKkEActLAmgZB1ZrtT6F\_e\_Jo2HJBmzZ61eP\-Nisp0cNiX7wGs\_axa5fjwR5KWyC4EWFyEPoDvDivFi46y9tz8citEr7u8F8K\-XoUqO\_pit1Tb6bDeGCr9peXXkxH5\_El9jYXUWq\_A4ZnEHkf&x\-client\-SKU=ID\_NETSTANDARD1\_4&x\-client\-ver=5\.2\.0\.0 200 OK (text/html)
AspNetZero Test:
OPTIONS https://dev\-586182\.oktapreview\.com/oauth2/default/v1/authorize?client\_id=0oagaur4ukBFH91Od0h7&redirect\_uri=http%3A%2F%2Flocalhost%3A62114%2Fauthorization\-code%2Fcallback&response\_type=code&scope=openid%20profile%20email&response\_mode=form\_post&nonce=636749484127536656\.NDc5OWM0MjAtNDExNy00NzAxLThkY2QtYjgyMTUyNDU4NDE0YzAyZDk5MzAtNTM3ZC00MDdiLThjYjctMjViYWNiNmRjYzVh&sessionToken=201115ITj3rXCMp473fVmULJCyQ4rpT3quS7wFqm9CYFVjSsWcMdLnH&state=CfDJ8LRmRAoWNcxFrJRw5HHQysSOL69ljb\_XmubbdQRu6u5yvUlhVRVcs9GnQDv4GZzwhPviVtZcBZWfu5cu746suqp2hGeW4wMy9qUzH9lIpzQaR3fHe4h9jRLCmS4YG48VUy5jBcCaz7oOhmRBD2FqmqyMcd3\_3gDRwU89UO9Acd5WEI\-0CQ\_mHy0GX24a9iVXNnCqftdPlPg8t\-4x501UiOtIfTo7J0HRk5cI\-WkX5R\_gJa5rp5lHG\-gMlQeEmKpBpBOmGFrEGBAJui0I\-RercQP0rAnFYg2s\_P5Oa1VrAC0U1sJ3TbMrbA9VYLBkiUM7K7rzueYh7os6uTsUeMDVv4qV3\_67oQM1BmjbKyzL7hw2iLSjVcuaaS5BbgxWyb\_QvgouezEHTsSbZcac0X1HrMu\-CZ01YnUMlaMfdJMHzbum&x\-client\-SKU=ID\_NET451&x\-client\-ver=5\.2\.0\.0 200 OK ()
I think the difference is that for some reason the Okta sample results in a get whereas the AspNetZero response results in an Options that I think is then failing because of cors.
Any thoughts? I know this one is tricky. I can send you both projects so that you can try it yourself.
Thanks, Craig
14 Answer(s)
-
0
+1
I have a madate to use Okta for authentication. @cmthomps can you share your code and also can you let me know how are you managing users and assigning them Tenants using Okta
-
0
Hi @cmthomps
I think you need to configure your org’s Trusted Origins to include url of your AspNet Zero app, see https://developer.okta.com/code/javascript/okta_sign-in_widget#enabling-cross-origin-access
-
0
Thanks @ismcagdas,
Can I send you my source code to take a look at? I've set-up the cors stuff. It's literally driving me crazy. I have a test project that I am trying to use as a proof of concept. What is the best way to do that?
-
0
@cmthomps
Just to be sure, have you configured it on okta side ?
-
0
@ismcagdas - Yes, I've configured the application in Okta. I have an okta asp.net core example project that works. And, I have an AspNetZero proof of concept project that I have modified. I'm stuck at the moment.
For the purposes of this proof of concept, I've modified the aspnetzero login page to render the Okta widget and I've modified the the account controller to handle the postback in the same way the okta example works.
-
0
Could you check this https://support.aspnetzero.com/QA/Questions/5773#answer-e8770448-1af0-3901-c258-39e98c3b7cd7 ?
If it doesn't help, please send your project to [email protected] with the necessary information and we will check it for you.
Thanks,
-
1
I'm getting close.
You're saying replace GetUserInfo in the method below with the method in the ticket you mentioned. The new method takes a "token" parameter. Where does that come from?
private async Task<ExternalAuthUserInfo> GetExternalUserInfo(ExternalAuthenticateModel model) { var userInfo = await _externalAuthManager.GetUserInfo(model.AuthProvider, model.ProviderAccessCode); if (userInfo.ProviderKey != model.ProviderKey) { throw new UserFriendlyException(L("CouldNotValidateExternalUser")); } return userInfo; }
-
0
@cmthomps I am currently trying to add custom claims to make the existing code work but I am struggling to get it right. If in the meantime you are able to use the custom code provided then let me know if it works.
-
0
Hi @cmthomps
okta login page should redirect you back to ExternalAuthenticate action of TokenAuthController with the token parameter added to url.
-
2
I finally figured this out. Just to be clear, I am replacing the default AspNetZero login experience with the Okta sign-in widget. (See screenshot). In addition to making changes to the login screen itself, I had to make the changes to the AuthConfigurer.cs and the AccountController.cs
The change to the AccountController was basic. Just made a new method to handle the postback from the Okta Widget:
[HttpPost] public ActionResult ExternalLoginOkta(string sessionToken = "") { string provider = "OpenIdConnect"; var redirectUrl = Url.Action( "ExternalLoginCallback", "Account", new { ReturnUrl = "/App", authSchema = provider, ss = "" }); var properties = new AuthenticationProperties(); //Note: the sessionToken is created by the Okta Widget and passed into this method. properties.Items.Add("sessionToken", sessionToken); properties.RedirectUri = redirectUrl; var challengeResponse = Challenge(properties, OktaDefaults.MvcAuthenticationScheme); return challengeResponse; }
I had to do some surgery on the AuthConfigurer.cs class. I added some event handler to OpenId to handle some apparent data either being in the incorrect place or missing from the data coming back from Okta.
public static void Configure(IServiceCollection services, IConfiguration configuration) { var authenticationBuilder = services.AddAuthentication(); //if (bool.Parse(configuration["Authentication:OpenId:IsEnabled"])) //{ // authenticationBuilder.AddOpenIdConnect(options => // { // options.ClientId = configuration["Authentication:OpenId:ClientId"]; // options.Authority = configuration["Authentication:OpenId:Authority"]; // options.SignedOutRedirectUri = configuration["App:WebSiteRootAddress"] + "Account/Logout"; // options.ResponseType = OpenIdConnectResponseType.IdToken; // var clientSecret = configuration["Authentication:OpenId:ClientSecret"]; // if (!clientSecret.IsNullOrEmpty()) // { // options.ClientSecret = clientSecret; // } // }); //} if (bool.Parse(configuration["Authentication:OpenId:IsEnabled"])) { authenticationBuilder.AddOpenIdConnect(oidcOptions => { oidcOptions.ClientId = configuration["Authentication:OpenId:ClientId"]; oidcOptions.Authority = configuration["Authentication:OpenId:Authority"]; oidcOptions.SignedOutRedirectUri = configuration["App:WebSiteRootAddress"] + "Account/Logout"; oidcOptions.ResponseType = OpenIdConnectResponseType.Code; oidcOptions.GetClaimsFromUserInfoEndpoint = true; oidcOptions.Scope.Add("openid"); oidcOptions.Scope.Add("profile"); oidcOptions.Scope.Add("email"); var clientSecret = configuration["Authentication:OpenId:ClientSecret"]; if (!clientSecret.IsNullOrEmpty()) { oidcOptions.ClientSecret = clientSecret; } oidcOptions.Events = new OpenIdConnectEvents() { //TODO This was the trick to get the Okta widget to work. OnRedirectToIdentityProvider = context => { // Add Okta sessionToken to provide custom login if (context.Properties.Items.TryGetValue("sessionToken", out var sessionToken)) { if (!string.IsNullOrEmpty(sessionToken)) { context.ProtocolMessage.SetParameter("sessionToken", sessionToken); } } return Task.CompletedTask; }, //TODO This is the only way i can get OpenId to work. OnTicketReceived = context => { if(!context.Properties.Items.ContainsKey("LoginProvider")) { //For some reason, the LoginProvider isn't being returned when using the Okta Widget context.Properties.Items.Add("LoginProvider", "OpenIdConnect"); } // Get the ClaimsIdentity var identity = context.Principal.Identity as ClaimsIdentity; if (identity != null) { // Add the Name ClaimType. This is required if we want User.Identity.Name to actually return something! if (!context.Principal.HasClaim(c => c.Type == ClaimTypes.Name) && identity.HasClaim(c => c.Type == "preferred_username")) identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identity.FindFirst("preferred_username").Value)); // Check if token names are stored in Properties if (context.Properties.Items.ContainsKey(".TokenNames")) { // Token names a semicolon separated string[] tokenNames = context.Properties.Items[".TokenNames"].Split(';'); // Add each token value as Claim foreach (var tokenName in tokenNames) { // Tokens are stored in a Dictionary with the Key ".Token.<token name>" string tokenValue = context.Properties.Items[$".Token.{tokenName}"]; identity.AddClaim(new Claim(tokenName, tokenValue)); } } } return Task.CompletedTask; } }; }); }
-
0
Do you mind sharing the code changes you did in the angular side ?
-
0
My project is .NET Core/jQuery. Sorry...
-
0
No problem, thank you for the response.
-
0
Thanks a million again @cmthomps for sharing this :)