diff --git a/README.md b/README.md index 87d56af..f37c045 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,16 @@ Please find the sample that fits your use-case from the table below. ## Contributing -We're happy to accept contributions and PRs! Please see the [contribution guide](CONTRIBUTING.md) to understand how to structure a contribution. \ No newline at end of file +We're happy to accept contributions and PRs! Please see the [contribution guide](CONTRIBUTING.md) to understand how to structure a contribution. + + +## Okta-Hosted-Login with dotnet48 MVC webapp and Okta OIDC +This webapp is able to authenticate with Okta and fetch back the user claims and id_token payload in the Owin context. However, the below issues are present and will not be readily resolved (MSFT Owin framework issue). Therefore, we would **not** recommend using dotnet48 with Okta OIDC. Instead, please upgrade to dotnet core to use Okta OIDC or if that is not possible, use dotnet48 with Okta SAML. + +## Known Issues +1. The Owin context does not contain the access token. It is null at runtime. This is a known issue with the Owin framework and is not resolved by Microsoft. Therefore, the access token cannot be fetched from the Owin context. If you need to call external APIs, this will be an issue. The proposed solution here is to create an OktaAdapter that will fetch and validate the access token. +2. Global signout does not work. This app's signout will result in a redirect to the global Okta org's configured error page. Instead, you may need to try Okta's Single Logout URL (see link below) or manually clearing the cookies, Okta session, and local session. If using JWT to manage user session, configure a low expiry access token and long refresh token approach. +3. Okta's prescribed solution with app.UseOktaMvc in Startup.cs does not work. It will result in an infinite redirect loop between the webapp and Okta's AuthZ server due to a thrown error. This looks to be an issue with Okta's aspnet library and dotnet48 Owin's middleware. Instead, I modified the service to instead call app.UseOpenIdConnectAuthentication directly - which works. I believe the user claims are pulled from the user-info endpoint though and not the id_token. + +## References +- [Okta Single Logout](https://help.okta.com/en-us/content/topics/apps/apps_single_logout.htm) \ No newline at end of file diff --git a/okta-hosted-login/okta-aspnet-mvc-example/Controllers/HomeController.cs b/okta-hosted-login/okta-aspnet-mvc-example/Controllers/HomeController.cs index e810189..fa12e8f 100644 --- a/okta-hosted-login/okta-aspnet-mvc-example/Controllers/HomeController.cs +++ b/okta-hosted-login/okta-aspnet-mvc-example/Controllers/HomeController.cs @@ -28,7 +28,18 @@ public ActionResult Contact() [Authorize] public ActionResult Profile() { - return View(HttpContext.GetOwinContext().Authentication.User.Claims); + var user = HttpContext.GetOwinContext().Authentication.User; + var claims = user.Claims; + var accessToken = user.Claims.FirstOrDefault(c => c.Type == "access_token")?.Value; + var idToken = user.Claims.FirstOrDefault(c => c.Type == "id_token")?.Value; + Console.WriteLine("This is accessToken: " + accessToken); // this will be null + Console.WriteLine("This is idToken: " + idToken); // this will be null + + ViewBag.AccessToken = accessToken; + ViewBag.IdToken = idToken; + + return View(claims); } + } } \ No newline at end of file diff --git a/okta-hosted-login/okta-aspnet-mvc-example/Startup.cs b/okta-hosted-login/okta-aspnet-mvc-example/Startup.cs index c212838..a73b0cc 100644 --- a/okta-hosted-login/okta-aspnet-mvc-example/Startup.cs +++ b/okta-hosted-login/okta-aspnet-mvc-example/Startup.cs @@ -1,23 +1,49 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Configuration; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web.Services.Description; +using Antlr.Runtime; +using IdentityModel.Client; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.OpenIdConnect; using Okta.AspNet; using Owin; +using static IdentityModel.OidcConstants; -[assembly: OwinStartup(typeof(okta_aspnet_mvc_example.Startup))] +[assembly: OwinStartup(typeof(dotnet48_okta_oidc_webapp.Startup))] -namespace okta_aspnet_mvc_example +namespace dotnet48_okta_oidc_webapp { public class Startup { public void Configuration(IAppBuilder app) { - app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); - - app.UseCookieAuthentication(new CookieAuthenticationOptions()); + // Enable TLS 1.2 + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + /* + app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); + + //app.UseCookieAuthentication(new CookieAuthenticationOptions() + //{ + // LoginPath = new PathString("/Account/Login"), + //}); + + app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions + { + AuthenticationType = "ApplicationCookie", + LoginPath = new PathString("/Account/Login") + }); + app.UseOktaMvc(new OktaMvcOptions() { OktaDomain = ConfigurationManager.AppSettings["okta:OktaDomain"], @@ -26,9 +52,97 @@ public void Configuration(IAppBuilder app) AuthorizationServerId = ConfigurationManager.AppSettings["okta:AuthorizationServerId"], RedirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"], PostLogoutRedirectUri = ConfigurationManager.AppSettings["okta:PostLogoutRedirectUri"], - GetClaimsFromUserInfoEndpoint = true, - Scope = new List {"openid", "profile", "email"}, + Scope = new List { "openid", "profile", "email" }, + //LoginMode = LoginMode.SelfHosted, + }); + */ + ConfigureAuth(app); + + + } + private void ConfigureAuth(IAppBuilder app) + { + app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); + + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = CookieAuthenticationDefaults.AuthenticationType, }); + + var clientId = ConfigurationManager.AppSettings["okta:ClientId"].ToString(); + var clientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"].ToString(); + var issuer = ConfigurationManager.AppSettings["okta:Issuer"].ToString(); + var redirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"].ToString(); + var postLogoutRedirectUri = ConfigurationManager.AppSettings["okta:PostLogoutRedirectUri"].ToString(); + UserInfoResponse userInfoResponse; + + app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret, + Authority = issuer, + RedirectUri = redirectUri, + ResponseType = "id_token token", + UseTokenLifetime = false, + Scope = "openid profile email", + PostLogoutRedirectUri = postLogoutRedirectUri, + TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "name" + }, + Notifications = new OpenIdConnectAuthenticationNotifications + { + RedirectToIdentityProvider = context => + { + if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout) + { + var idToken = context.OwinContext.Authentication.User.Claims.FirstOrDefault(c => c.Type == "id_token")?.Value; + context.ProtocolMessage.IdTokenHint = idToken; + } + + return Task.FromResult(true); + }, + AuthorizationCodeReceived = async context => + { + // Exchange code for access and ID tokens + var tokenClient = new TokenClient( + issuer + "/v1/token", clientId, clientSecret); + var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(context.ProtocolMessage.Code, redirectUri); + + if (tokenResponse.IsError) + { + throw new Exception(tokenResponse.Error); + } + + var userInfoClient = new UserInfoClient(issuer + "/v1/userinfo"); + userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken); + + var identity = new ClaimsIdentity(); + identity.AddClaims(userInfoResponse.Claims); + identity.AddClaim(new Claim("id_token", tokenResponse.IdentityToken)); + identity.AddClaim(new Claim("access_token", tokenResponse.AccessToken)); + if (!string.IsNullOrEmpty(tokenResponse.RefreshToken)) + { + identity.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken)); + } + + var nameClaim = new Claim(ClaimTypes.Name, userInfoResponse.Claims.FirstOrDefault(c => c.Type == "name")?.Value); + identity.AddClaim(nameClaim); + + + context.AuthenticationTicket = new AuthenticationTicket( + new ClaimsIdentity(identity.Claims, context.AuthenticationTicket.Identity.AuthenticationType), + context.AuthenticationTicket.Properties); + + Console.WriteLine("This is tokenResponse.AccessToken: "); + Console.WriteLine(tokenResponse.AccessToken); + Trace.WriteLine("This is tokenResponse.AccessToken: "); + Trace.WriteLine(tokenResponse.AccessToken); + } + } + }); + Console.WriteLine("Okta OpenID Connect middleware registered."); } + } -} +} \ No newline at end of file diff --git a/okta-hosted-login/okta-aspnet-mvc-example/Views/Home/Profile.cshtml b/okta-hosted-login/okta-aspnet-mvc-example/Views/Home/Profile.cshtml index b639c61..5032b8e 100644 --- a/okta-hosted-login/okta-aspnet-mvc-example/Views/Home/Profile.cshtml +++ b/okta-hosted-login/okta-aspnet-mvc-example/Views/Home/Profile.cshtml @@ -6,7 +6,9 @@

@ViewBag.Title

-
+@if (Context.User.Identity.IsAuthenticated) +{ +
@foreach (var claim in Model) {
@@ -22,4 +24,9 @@
@claim.Value
} -
\ No newline at end of file +
+} else { + +} \ No newline at end of file diff --git a/okta-hosted-login/okta-aspnet-mvc-example/Views/Shared/_Layout.cshtml b/okta-hosted-login/okta-aspnet-mvc-example/Views/Shared/_Layout.cshtml index 6fee3ce..b8d5783 100644 --- a/okta-hosted-login/okta-aspnet-mvc-example/Views/Shared/_Layout.cshtml +++ b/okta-hosted-login/okta-aspnet-mvc-example/Views/Shared/_Layout.cshtml @@ -1,5 +1,6 @@  - + + @@ -7,6 +8,7 @@ @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") +