I have been working on this pretty much all day. The Github demo seems to be a http/https-thing. After I discovered this, I dismissed it and retried my main project.
I am still not able to reproduce the issue on command, but if I clear my browser caches or switch between browsers a couple of times, it will get stuck in the loop on the 2nd or 3rd attempt almost always, also in localhost, which means I have been somewhat able to debug it for a little bit!
Since the demo project worked, I have for now also dismissed the idea that something is configured weird in Okta. I have compared NuGets, but it looks like all the relevant packages are up to date in both projects. I have copy-pasted the Startup.cs file from the demo project, and added a few lines to get some additional claims, but adding these lines also worked in the demo project. AppSettings are updated with correct URLs as well.
I have put breakpoints in the AuthorizationCodeReceived and RedirectToIdentityProvider in the Startup file, and it will jump back and forth between these two during the loop. This will happen during a redirect from the Okta login page back to my pages.
Using the Google Chrome Network tab, I have also captured a list of the URLs being called in a successful login VS unsuccessful:
Successful
MyPage (status code 302)
authorize?client_id=0oa1…
login.html?form=URI=…
… (various png, jpeg, font)
authn
sessionCookieRedirect?checkAccountSet…
redirect?okta_key=wYm…
callback
MyPage (status code 200)
Unsuccessful
MyPage (status code 302)
authorize?client_id=0oa1…
login.html?form=URI=…
… (various png, jpeg, font)
authn
sessionCookieRedirect?checkAccountSet…
redirect?okta_key=wYm…
callback
MyPage (status code 302)
authorize?client_id=0oa1…
callback
MyPage (status code 302)
authorize?client_id=0oa1…
callback
MyPage (status code 302)
… and so on.
I have also added a filter/attribute to handle roles, called “OktaAuthorize”. This is the biggest change between my main project and my biggest suspicion when it comes to whats messing things up, although it does not do much more than just check claims and get the role of the user. If I use the OktaAuthorize attribute instead of Authorize and set a breakpoint in it, it will be added to the redirect loop, along with AuthorizationCodeReceived and RedirectToIdentityProvider, and I can see that the HttpContext.Current.GetOwinContext().Authentication.User.Claims returns an empty list. On a successful login, this list includes all the claims from the Startup.cs.
PS: Both the OktaAuthorize and the normal Authorize filters can make the login an infinite loop.
Here is my OktaAuthorize file:
public class OktaAuthorizeAttribute : AuthorizeAttribute
{
public TechstepUserRole UserRole { get; private set; }
public OktaAuthorizeAttribute() : this (TechstepUserRole.All) { }
public OktaAuthorizeAttribute(TechstepUserRole userRole)
{
UserRole = userRole;
}
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
{
var claims = HttpContext.Current.GetOwinContext().Authentication.User.Claims;
// Check claims and get the role
bool valid = Verify(claims);
if (claims.Any() && !valid)
{
var result = new ContentResult { Content = "unauthorized" };
filterContext.Result = result;
}
else if (valid)
{
return;
}
else
{
base.OnAuthorization(filterContext);
}
}
}
And here is my Startup.cs, just incase:
public class Startup
{
// These values are stored in Web.config. Make sure you update them!
private readonly string clientId = ConfigurationManager.AppSettings[“okta:ClientId”];
private readonly string redirectUri = ConfigurationManager.AppSettings[“okta:RedirectUri”];
private readonly string authority = ConfigurationManager.AppSettings[“okta:OrgUri”];
private readonly string clientSecret = ConfigurationManager.AppSettings[“okta:ClientSecret”];
private readonly string postLogoutRedirectUri = ConfigurationManager.AppSettings[“okta:PostLogoutRedirectUri”];
/// <summary>
/// Configure OWIN to use OpenID Connect to log in with Okta.
/// </summary>
/// <param name="app"></param>
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
ClientSecret = clientSecret,
Authority = authority,
RedirectUri = redirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = OpenIdConnectScope.OpenIdProfile,
PostLogoutRedirectUri = postLogoutRedirectUri,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Exchange code for access and ID tokens
var tokenClient = new TokenClient(authority + "/v1/token", clientId, clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var userInfoClient = new UserInfoClient(authority + "/v1/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<Claim>();
claims.AddRange(userInfoResponse.Claims);
claims.Add(new Claim("id_token", tokenResponse.IdentityToken));
claims.Add(new Claim("access_token", tokenResponse.AccessToken));
// Get claims from Access Token
var handler = new JwtSecurityTokenHandler();
var tkn = handler.ReadToken(tokenResponse.AccessToken) as JwtSecurityToken;
var mytosSubsId = tkn.Claims.FirstOrDefault(c => c.Type == "mytosSubsId") ?? new Claim("mytosSubsId", "");
claims.Add(new Claim("mytos_subs_id", mytosSubsId.Value));
var agentSalesId = tkn.Claims.FirstOrDefault(c => c.Type == "agentSalesId") ?? new Claim("agentSalesId", "");
claims.Add(new Claim("agent_sales_id", agentSalesId.Value));
var techstepUserId = tkn.Claims.FirstOrDefault(c => c.Type == "techstepUserId") ?? new Claim("techstepUserId", "");
claims.Add(new Claim("techstep_user_id", techstepUserId.Value));
if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
{
claims.Add(new Claim("refresh_token", tokenResponse.RefreshToken));
}
n.AuthenticationTicket.Identity.AddClaims(claims);
return;
},
RedirectToIdentityProvider = n =>
{
// If signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenClaim != null)
{
n.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
}
}
return Task.CompletedTask;
}
},
});
}
}
Hope you can spot something that may cause some trouble!