I have an ASP.NET 4.6 web app that I'm trying to add OpenId Connect using OWIN.
I added my Owin startup class and everything appears to be configured correctly, but the problem I'm having is the ASP Identity/Authenticated user never gets created. I end up with an endless loop, where the OpenId callback page redirects back to the original page, which then redirects to the login page, etc.
Here is my startup class:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login.aspx"),
ExpireTimeSpan = TimeSpan.FromDays(7)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = _clientId,
ClientSecret = _clientSecret,
Authority = _authority,
RedirectUri = _redirectUri, // LoginCallback
PostLogoutRedirectUri = "http://localhost:60624/Logout.aspx",
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = "openid profile email",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Exchange code for access and ID tokens
var tokenClient = new TokenClient($"{_authority}/as/token.oauth2", _clientId, _clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, _redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var userInfoClient = new UserInfoClient($"{_authority}/idp/userinfo.openid");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<Claim>(userInfoResponse.Claims)
{
new Claim("id_token", tokenResponse.IdentityToken),
new Claim("access_token", tokenResponse.AccessToken)
};
n.AuthenticationTicket.Identity.AddClaims(claims);
//// create the identity
//var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
//System.Web.HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
//{
// IsPersistent = true
//}, identity);
}
}
});
}
Here is the Login.aspx page:
protected void Page_Load(object sender, EventArgs e)
{
if (!Request.IsAuthenticated)
{
HttpContext.Current.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = Request["ReturnUrl"] ?? "Default.aspx" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
The page flow goes like this:
1) Request: http://localhost:60624/Page.aspx Response: 302 - redirect to Login.aspx
2) Request: http://localhost:60624/Login.aspx?ReturnUrl=%2FPage.aspx Response 302 - redirect to https://auth.myprovider.com
Some cookies set here on the response headers:
Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=xxxxxxxxx; path=/; expires=Mon, 22-Apr-2019 14:12:00 GMT; HttpOnly Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=yyyyyyyyy; expires=Mon, 22-Apr-2019 14:12:00 GMT; path=/; HttpOnly
3) Auth provider, sign-in, and it 302 redirects to /LoginCallback
4) Request: http://localhost:60624/LoginCallback Response 302 - redirect to /Page.aspx
Cookies that were set in step 2 are cleared here.
Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT Set-Cookie: OpenIdConnect.nonce.KIsuj4RUmGKJIynLrkEScxBvGrZzkMo6ylZ%2F4lRknPM%3D=; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=/
5) Back to Page.aspx, user not authenticated; Goto step 1
I've done some debugging, and the AuthorizationCodeReceived fires on the Startup, and the backend successfully calls the User Info endpoint. I've tried to call System.Web.HttpContext.Current.GetOwinContext().Authentication.SignIn() from that Notification, but that doesn't seem to do anything.
At this point, I'm stuck. Why is the authentication cookie for the user identity not being set? It seems like this is supposed to happen automatically. Am I supposed to manually create this myself? (How can I manually create a authentication cookie instead of the default method?)
EDIT: After reviewing @Zaxxon's reply, I was able to get it working. There were 2 things wrong in the AuthorizationCodeReceived notification
Here is the working code:
ClaimsIdentity identity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.GivenName, ClaimTypes.Role);
n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);
n.AuthenticationTicket.Identity.AddClaims(claims);
Yep, I had to get a Proof of Concept working for this a while back in VB.Net which was somewhat painful. Here is my test code (i.e. not production code), which is based on some other C# internet examples I saw:
Imports System.Security.Claims
Imports System.Threading.Tasks
Imports IdentityModel
Imports IdentityModel.Client
Imports Microsoft.AspNet.Identity
Imports Microsoft.AspNet.Identity.Owin
Imports Microsoft.IdentityModel.Protocols.OpenIdConnect
Imports Microsoft.Owin
Imports Microsoft.Owin.Security
Imports Microsoft.Owin.Security.Cookies
Imports Microsoft.Owin.Security.Notifications
Imports Microsoft.Owin.Security.OAuth
Imports Microsoft.Owin.Security.OpenIdConnect
Imports Owin
Partial Public Class Startup
Private Shared _oAuthOptions As OAuthAuthorizationServerOptions
Private Shared _publicClientId As String
Private Shared _clientId As String
Private Shared _clientSecret As String
' Enable the application to use OAuthAuthorization. You can then secure your Web APIs
Shared Sub New()
_clientId = System.Configuration.ConfigurationManager.AppSettings("OAuth:ClientID").ToString()
_clientSecret = System.Configuration.ConfigurationManager.AppSettings("OAuth:SecretKey").ToString()
PublicClientId = _clientId
OAuthOptions = New OAuthAuthorizationServerOptions() With {
.TokenEndpointPath = New PathString("/Token"), 'New PathString("https://authtesteria.domain.com/as/token.oauth2"), '
.AuthorizeEndpointPath = New PathString("/Account/Authorize"), 'New PathString("https://authtesteria.domain.com/as/authorization.oauth2"), '
.Provider = New ApplicationOAuthProvider(PublicClientId),
.AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
.AllowInsecureHttp = True
}
End Sub
Public Shared Property OAuthOptions() As OAuthAuthorizationServerOptions
Get
Return _oAuthOptions
End Get
Private Set
_oAuthOptions = Value
End Set
End Property
Public Shared Property PublicClientId() As String
Get
Return _publicClientId
End Get
Private Set
_publicClientId = Value
End Set
End Property
' For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
Public Sub ConfigureAuth(app As IAppBuilder)
' Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(AddressOf ApplicationDbContext.Create)
app.CreatePerOwinContext(Of ApplicationUserManager)(AddressOf ApplicationUserManager.Create)
app.CreatePerOwinContext(Of ApplicationSignInManager)(AddressOf ApplicationSignInManager.Create)
' Enable the application to use a cookie to store information for the signed in user
' and to use a cookie to temporarily store inforation about a user logging in with a third party login provider
' Configure the sign in cookie
' OnValidateIdentity enables the application to validate the security stamp when the user logs in.
' This is a security feature which is used when you change a password or add an external login to your account.
app.UseCookieAuthentication(New CookieAuthenticationOptions() With {
.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
.Provider = New CookieAuthenticationProvider() With {
.OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(Of ApplicationUserManager, ApplicationUser)(
validateInterval:=TimeSpan.FromMinutes(30),
regenerateIdentity:=Function(manager, user) user.GenerateUserIdentityAsync(manager))},
.LoginPath = New PathString("/Account/Login")})
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie)
' Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5))
' Enables the application to remember the second login verification factor such as phone or email.
' Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
' This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie)
' Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions)
Dim controller As New AccountController()
'Dim validator As OpenIdConnectProtocolValidator = New OpenIdConnectProtocolValidator()
'validator.ShowPII = False
Dim oidcAuth As New Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions() With {
.ClientId = _clientId,
.ClientSecret = _clientSecret,
.Authority = "https://authtesteria.domain.com",
.Notifications = New Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationNotifications() With {
.RedirectToIdentityProvider = AddressOf OnRedirectToIdentityProvider,
.MessageReceived = AddressOf OnMessageReceived,
.SecurityTokenReceived = AddressOf OnSecurityTokenReceived,
.SecurityTokenValidated = AddressOf OnSecurityTokenValidated,
.AuthorizationCodeReceived = AddressOf OnAuthorizationCodeReceived,
.AuthenticationFailed = AddressOf OnAuthenticationFailed
}}
app.UseOpenIdConnectAuthentication(oidcAuth)
End Sub
Private Function OnRedirectToIdentityProvider(arg As RedirectToIdentityProviderNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
Debug.WriteLine("*** RedirectToIdentityProvider")
If arg.ProtocolMessage.RequestType = Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout Then
Dim idTokenHint = arg.OwinContext.Authentication.User.FindFirst("id_token")
If idTokenHint IsNot Nothing Then
arg.ProtocolMessage.IdTokenHint = idTokenHint.Value
End If
End If
Return Task.FromResult(0)
End Function
Private Function OnMessageReceived(arg As MessageReceivedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
Debug.WriteLine("*** MessageReceived")
Return Task.FromResult(0)
End Function
Private Function OnAuthorizationCodeReceived(arg As AuthorizationCodeReceivedNotification) As Task
Debug.WriteLine("*** AuthorizationCodeReceived")
'Upon successful sign in, get & cache a token if you want here
Return Task.FromResult(0)
End Function
Private Function OnAuthenticationFailed(arg As AuthenticationFailedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
Debug.WriteLine("*** AuthenticationFailed")
Return Task.FromResult(0)
End Function
Private Function OnSecurityTokenReceived(arg As SecurityTokenReceivedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
Debug.WriteLine("*** SecurityTokenReceived")
Return Task.FromResult(0)
End Function
Private Async Function OnSecurityTokenValidated(arg As SecurityTokenValidatedNotification(Of Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage, OpenIdConnectAuthenticationOptions)) As Task
Debug.WriteLine("*** SecurityTokenValidated")
'Verify the user signing in should have access or not. Here I just pass folk thru.
Dim nid = New ClaimsIdentity(
DefaultAuthenticationTypes.ApplicationCookie, 'arg.AuthenticationTicket.Identity.AuthenticationType,
ClaimTypes.GivenName,
ClaimTypes.Role)
Dim tokenClient = New TokenClient("https://authtesteria.domain.com/as/token.oauth2",
_clientId,
_clientSecret)
Dim tokenResponse = Await tokenClient.RequestAuthorizationCodeAsync(arg.ProtocolMessage.Code, arg.ProtocolMessage.RedirectUri)
' get userinfo data
Dim userInfoClient = New IdentityModel.Client.UserInfoClient("https://authtesteria.domain.com/idp/userinfo.openid")
Dim userInfo = Await userInfoClient.GetAsync(tokenResponse.AccessToken)
userInfo.Claims.ToList().ForEach(Sub(ui) nid.AddClaim(New Claim(ui.Type, ui.Value)))
'' keep the id_token for logout
'nid.AddClaim(New Claim("id_token", arg.ProtocolMessage.IdToken))
'' add access token for sample API
'nid.AddClaim(New Claim("access_token", arg.ProtocolMessage.AccessToken))
'' keep track of access token expiration
'nid.AddClaim(New Claim("expires_at", DateTimeOffset.Now.AddSeconds(Integer.Parse(arg.ProtocolMessage.ExpiresIn)).ToString()))
'' add some other app specific claim
'nid.AddClaim(New Claim("app_specific", "some data"))
nid.AddClaim(New Claim(ClaimTypes.Role, "group1"))
arg.AuthenticationTicket = New AuthenticationTicket(nid, arg.AuthenticationTicket.Properties)
arg.AuthenticationTicket.Properties.RedirectUri = HttpContext.Current.Session("PageRedirect").ToString()
End Function
End Class
Now I trigger the login like so:
Private Sub SomePageName_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then
If User.Identity.IsAuthenticated Then
Console.WriteLine(User.Identity.GetUserName())
Else
Session("PageRedirect") = Request.Url
Response.Redirect("/")
End If
End If
End Sub
We have a few differences:
Hopefully this will be of some help. Enjoy!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With