Reverse proxy with openid connect redirection - c#

In my application I have integrated Identity server 3 with openid-connect.
On our production server our website is behind a reverse proxy which is causing problems;
When the user logs in and is redirected back by identity server, our application wants to redirect the user to his original location (the page with the AuthorizeAttribute).
The problem here is that the user is redirected to the hidden url instead of the public url used by the reverse proxy.
How can I redirect the user to the public url?

After a long search this is the fix:
The OWIN middleware UseOpenIdConnectAuthentication has a property Notifications in the Options property.
This Notifications property has a func SecurityTokenValidated. In this function you can modify the Redirect Uri.
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
Authority = "https://idp.io",
ClientId = "clientid",
RedirectUri = "https://mywebsite.io",
ResponseType = "code id_token token",
Scope = "openid profile",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = notification =>
{
notification.AuthenticationTicket.Properties.RedirectUri = RewriteToPublicOrigin(notification.AuthenticationTicket.Properties.RedirectUri);
return Task.CompletedTask;
}
}
});
This is the function which rewrites the url to the public origin:
private static string RewriteToPublicOrigin(string originalUrl)
{
var publicOrigin = ConfigurationManager.AppSettings["app:identityServer.PublicOrigin"];
if (!string.IsNullOrEmpty(publicOrigin))
{
var uriBuilder = new UriBuilder(originalUrl);
var publicOriginUri = new Uri(publicOrigin);
uriBuilder.Host = publicOriginUri.Host;
uriBuilder.Scheme = publicOriginUri.Scheme;
uriBuilder.Port = publicOriginUri.Port;
var newUrl = uriBuilder.Uri.AbsoluteUri;
return newUrl;
}
return originalUrl;
}
Now the OpenIdConnect redirects the user to the public url instead of the non-public webserver url.

Related

SSO Request.IsAuthenticated is always false after the first Login, however the first SSO login works fine

Please forgive me if upfront if I missed something or made a mistake. I think I've only had to post here a couple of times....
I have googled this situation for two days trying this and that and am no closer to having this solved. Something in Chrome changed and it broke my app. The scenario is I have an MVC 5 app that uses SSO. The first login brings me to the microsoftonline login page, and I can login successfully - upon which I am brought to my application redirectURI page and Request.IsAuthenticated = true. All is good. However, if I either close the browser or use the Log Out link (which executes the Logout code below) and try to access my app again, I am brought to microsoftonline login page as expected, enter my password, but the 2nd time Request.IsAuthenticated = false and my app no longer works. It expects that Request.IsAuthenticated will be true, and because it is false it redirects back to microsoftonline's login page again resulting in a constant loop. I have found that I can restart the web site and it somehow resets the Request.IsAuthenticated so I can login again.
I have no more ideas how to fix this. Any help is greatly appreciated.
Here is the SSOAuthConfig: (it's basically a carbon copy of the Azure App Registration ASP.Net Quick Start example)
internal static class SSOAuthConfig2020
{
// The Client ID is used by the application to uniquely identify itself to Azure AD.
static string clientId = System.Configuration.ConfigurationManager.AppSettings["ClientId"];
// RedirectUri is the URL where the user will be redirected to after they sign in.
static string redirectUri = System.Configuration.ConfigurationManager.AppSettings["RedirectUri"];
// Tenant is the tenant ID (e.g. contoso.onmicrosoft.com, or 'common' for multi-tenant)
static string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
// Authority is the URL for authority, composed by Microsoft identity platform endpoint and the tenant name (e.g. https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0)
static string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
/// <summary>
/// Configure OWIN to use OpenIdConnect
/// </summary>
/// <param name="app"></param>
public static void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
var cookieAuthenticationOptions = new CookieAuthenticationOptions()
{
CookieName = "MyFakeCookieName",
ExpireTimeSpan = TimeSpan.FromDays(1),
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
SlidingExpiration = true,
};
app.UseCookieAuthentication(cookieAuthenticationOptions);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.IdToken,
// ValidateIssuer set to false to allow personal and work accounts from any organization to sign in to your application
// To only allow users from a single organizations, set ValidateIssuer to true and 'tenant' setting in web.config to the tenant name
// To allow users from only a list of specific organizations, set ValidateIssuer to true and use ValidIssuers parameter
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true
},
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = async n =>
{
n.AuthenticationTicket.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(60);
n.AuthenticationTicket.Properties.IsPersistent = true;
n.AuthenticationTicket.Properties.AllowRefresh = true;
n.AuthenticationTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow;
}
}
}
);
}
Here is the Login logic:
public void SignIn()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/Client" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
Here is the Log Out:
public void SignOut()
{
try
{
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
}
catch (Exception ex)
{
}
}
I found the answer to my own question several months after asking it. I posted the answer to help this person and am just linking to it to avoid duplication:
Getting NULL Identity while authenticating user via Azure AD authentication

CORS issue with UseOpenIdConnectAuthentication

Here Angularjs is front end and Web API is middle tier. we are using AzureAD OpenID connect for Authentication.
I'm facing following issue. because of the my landing page is not loading
Access to XMLHttpRequest at 'https://login.microsoftonline.com/xx-86f1-41af-91ab-xxx/oauth2/authorize?client_id=xxxx1&response_mode=form_post&response_type=code%20id_token&scope=openid%20profile&state=OpenIdConnect.AuthenticationPropertiesxxxxx&noncexxxxx&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2F&x-client-SKU=ID_NET451&x-client-ver=5.2.1.0' (redirected from 'https%3A%2F%2Flocalhost%3A44300%2F/api/Scorecard/GetServiceNameWithManagers?loginUser=xxx#microsoft.com') from origin 'https%3A%2F%2Flocalhost%3A44300%2F' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I've done lot of research and applied Access-Control-Allow-Origin =* at Request and response. also applied app.UseCors(Owin.Cors.CorsOptions.AllowAll);
but so far no success.
consider following code, AuthorizationCodeReceived delegate is not invoking very first time even though the user is logged in to microsoft site.
Please be noted, this code is not working very first time. It will work after few button clicks (postbacks) and then after few minutes if we run the application it's throws CORS preflight issue. Please help.
This is my startup.cs
public void Configuration(IAppBuilder app)
{
app.UseCors(Owin.Cors.CorsOptions.AllowAll);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new SystemWebChunkingCookieManager(),
});
//// Bearer token authentication middleware.(Ex: request from web clients,ajax calls able to pass authenticated bearer info)
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
TokenReplayCache = new TokenReplayCache(new MemoryCacheProvider())
},
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = ctx =>
{
//// Retrieve user roles from the request.
var authenticationTicket = ctx.Ticket;
if (authenticationTicket.Identity.IsAuthenticated)
{
////Use the block when role/user specific authorization needs and to modify the user identity claims based on requirement
}
return Task.FromResult(0);
},
OnRequestToken = ctx => { return Task.FromResult(0); }
}
});
//// Non Bearer authentication middleware. (Ex: request secured web api call directly from URL/Web API server scope it self)
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
ClientSecret = ConfigurationManager.AppSettings["ida:AppKey"],
Authority = Authority,
PostLogoutRedirectUri = PostLogoutRedirectUri,
AuthenticationMode = AuthenticationMode.Active,
ResponseType = "code id_token",
CallbackPath = new PathString("/"),
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = context =>
{
if (context.AuthenticationTicket.Identity.IsAuthenticated)
{
////Use the block when role/user specific authorization needs and to modify the user identity claims based on requirement
}
return Task.FromResult(0);
},
AuthorizationCodeReceived = async context =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(ClientId, Models.ConfigurationData.GraphSecret);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, credential, GraphResource);
},
RedirectToIdentityProvider = context =>
{
if (context.ProtocolMessage.RedirectUri == null)
{
////To set the reply/redirect Url based on the request host environment.
////Hosting env details we get only through the owin context in startup and this is the delegate to set reply URL in OWincontext before the authentication.
string ReplyAddress = context.Request.Scheme + "://" + context.Request.Host + "/";
context.ProtocolMessage.RedirectUri = ReplyAddress;
}
//context.OwinContext.Authentication.User.Identity.IsAuthenticated = true;
if (context.OwinContext.Authentication.User.Identity.IsAuthenticated && context.ProtocolMessage.RequestType != IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
{
////To avoid infinite loop of redirections in request if user is authenticated and unauthorized.
context.HandleResponse();
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return Task.FromResult(0);
}
},
TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles",
TokenReplayCache = new TokenReplayCache(new MemoryCacheProvider())
},
});
System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = System.IdentityModel.Claims.ClaimTypes.NameIdentifier;
}
Access to XMLHttpRequest at 'https://login.microsoftonline.com/xx-86f1-41af-91ab-xxx/oauth2/authorize?client_id=xxxx1&response_mode=form_post&response_type=code%20id_token&scope=openid%20profile&state=OpenIdConnect.AuthenticationPropertiesxxxxx&noncexxxxx&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2F&x-client-SKU=ID_NET451&x-client-ver=5.2.1.0' (redirected from 'https%3A%2F%2Flocalhost%3A44300%2F/api/Scorecard/GetServiceNamxxxManagers?loginUser=xxx#microsoft.com') from origin 'https%3A%2F%2Flocalhost%3A44300%2F' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I think you misunderstood the error message. It says that your AngularJS application was trying to make a request to https://login.microsoftonline.com/xxx-xx-41af-91ab-xxx/oauth2/authorize and failed, because it was a cross-origin request and the server didn't approve it by returning the Access-Control-Allow-Origin header in a response to preflight request (HTTP method OPTIONS).
So you cannot change it by adding CORS headers to your backend. The authorize is not designed to be called by XMLHttpRequest requests - you are supposed to make a full browser request to that URL. Later, the browser will be redirected to redirect_uri (the request parameter value) along with an auth code or an error.

Adding SSO OpenId/Azure AD auth to an existing Web Forms app

I have a web forms app currently using either forms authentication (or LDAP which then sets a FormsAuthenticationTicket cookie). I need to add SSO to this project and I'm currently using OpenID/Azure AD to authenticate with. I have the following Startup.cs configured.
public void Configuration(IAppBuilder app)
{
string appId = "<id here>";
string aadInstance = "https://login.microsoftonline.com/{0}";
string tenant = "<tenant here>";
string postLogoutRedirectUri = "https://localhost:21770/";
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenReceived = context =>
{
System.Diagnostics.Debug.WriteLine("SecurityTokenReceived");
return Task.FromResult(0);
},
SecurityTokenValidated = async n =>
{
var claims_to_exclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claims_to_keep =
n.AuthenticationTicket.Identity.Claims
.Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
if (n.ProtocolMessage.AccessToken != null)
{
claims_to_keep.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));
//var userInfoClient = new UserInfoClient(new Uri("https://localhost:44333/core/connect/userinfo"), n.ProtocolMessage.AccessToken);
//var userInfoResponse = await userInfoClient.GetAsync();
//var userInfoClaims = userInfoResponse.Claims
// .Where(x => x.Item1 != "sub") // filter sub since we're already getting it from id_token
// .Select(x => new Claim(x.Item1, x.Item2));
//claims_to_keep.AddRange(userInfoClaims);
}
var ci = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
"name", "role");
ci.AddClaims(claims_to_keep);
n.AuthenticationTicket = new AuthenticationTicket(
ci, n.AuthenticationTicket.Properties
);
},
MessageReceived = context =>
{
System.Diagnostics.Debug.WriteLine("MessageReceived");
return Task.FromResult(0);
},
AuthorizationCodeReceived = context =>
{
System.Diagnostics.Debug.WriteLine("AuthorizationCodeReceived");
return Task.FromResult(0);
},
AuthenticationFailed = context =>
{
System.Diagnostics.Debug.WriteLine("AuthenticationFailed");
context.HandleResponse();
context.Response.Write( context.Exception.Message);
return Task.FromResult(0);
}
,
RedirectToIdentityProvider = (context) =>
{
System.Diagnostics.Debug.WriteLine("RedirectToIdentityProvider");
//string currentUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.Path;
//context.ProtocolMessage.RedirectUri = currentUrl;
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
}
I have placed this in page Load event of my master (although it never seems to be getting hit - something else must be causing the authentication process to kick off when I navigate to a page requiring authentication.)
if (!Request.IsAuthenticated)
{
HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
My Azure settings are all correct because I am hitting SecurityTokenValidated and AuthorizationCodeReceived functions - I can see my email I am logged in with in the claims information, but I am not sure what to do next. As is I have a never ending loop of authentication requests. I am assuming this is because I have not translated the claim information I have received back into forms authentication ? I attempted to add a dummy auth ticket to the response in AuthorizationCodeReceived but that didn't appear to change anything - I am still getting the looping authentication requests.
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, "<UserName>", DateTime.Now, DateTime.Now.AddMinutes(60), true,"");
String encryptedTicket = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);
This is not a clear cut answer but it's too big for a comment.
I'm using "Organizational Accounts" (i.e. O365 email logins) and I had two big problems (both solved).
First Issue
intermittently, when logging in it would go into an endless redirect loop back and forth between two pages (This didn't happen all the time - only after half an hour testing and logging in and out).
If I left it long enough it would say "query string too long". There is a lot of long winded explanation around cookies and stuff but I had difficulties solving it. In the end it was solved simply by forcing https instead of http
I don't think that's your issue as it seems like it does it everytime. Perhaps have a read through this
New Asp.Net MVC5 project produces an infinite loop to login page
One answer says:
Do not call a protected web API (any web API which requires
Authorization) from an authorization page such as ~/Account/Login
(which, by itself, does NOT do this.). If you do you will enter into
an infinite redirect loop on the server-side.
Second Issue
So the next thing was: our existing authorisation system was sitting in a classic login/pwd table in our database (with an unencrypted password field >:| ). So I needed to pick up the login email and match that to a role defined in this table. Which I did thanks to the guy who answered my question:
Capturing login event so I can cache other user information
This answer meant that I could:
Go pick up the users role from the database once upon initial login
Save this role inside the existing native C# security object
Best of all: use the native authorisation annotations in my controller methods without any custom code in the method
I think thats what you are after but the question really is: how are you currently storing roles? In a database table? In Active Directory? In Azure active directory?
So in the hope that it help someone else - this is what I ended up with. In the web.config, the authentication mode is set to 'Forms'. I added the following Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var appId = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_APPID);
var authority = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_AUTHORITY);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
string username = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(60), true, "");
String encryptedTicket = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);
return Task.FromResult(0);
},
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
}
}
});
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
}
I did not add any challenge to my site master pages and instead added the following to my login page to trigger the authentication challenge:
if (!Request.IsAuthenticated && AttemptSSO)
{
ReturnURL = Request.QueryString["ReturnUrl"];
HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
else if (Request.IsAuthenticated && AttemptSSO)
{
if (!string.IsNullOrEmpty(ReturnURL))
{
var url = ReturnURL;
ReturnURL = "";
Response.Redirect(ResolveUrl(url));
}
else
{
Response.Redirect(ResolveUrl("~/Default.aspx"));
}
}
This means that if a user arrives at a authenticated page without a valid forms authentication token they get redirected to the login page. The login page takes care of deciding if SSO is set up and handling it appropriately. If anyone has any thoughts as to how to improve it - I'd love to hear them, but for the moment this does work.

HttpClient to call Azure AD-protected site

Following some Microsoft samples, I got to this point:
ASP.NET Core setup:
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAD:ClientId"],
Authority = Configuration["Authentication:AzureAd:Authority"],
ResponseType = OpenIdConnectResponseType.IdToken,
AutomaticAuthenticate = true,
TokenValidationParameters = new TokenValidationParameters()
});
AuthorizationTest endpoint:
[HttpGet]
[Authorize]
public IActionResult Get()
{
return Ok("SAMPLE TEXT - if you can read this then call it a day :)");
}
Client:
try
{
var result = await authContext.AcquireTokenAsync(WebApiResourceId, WebApiClientId, WebApiRedirectUri, new PlatformParameters(PromptBehavior.Auto));
authorizedClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
var authorizedMessage = await authorizedClient.GetAsync("/AuthorizationTest");
var statusCode = authorizedMessage.StatusCode.ToString();
var message = await authorizedMessage.Content.ReadAsStringAsync();
webBrowser.NavigateToString(message);
}
And the authorizedClient is initiated as:
private static HttpClientHandler handler = new HttpClientHandler
{
AllowAutoRedirect = true,
CookieContainer = new CookieContainer(),
UseCookies = true
};
private static HttpClient authorizedClient = new HttpClient(handler, false) { BaseAddress = WebApiBaseUri };
I used to initialize it only with the BaseAddress, and later added the handler following an answer here on So.
The problem:
Even though I get the token from AAD correctly, the response from the WEB API endpoint is an HTML (after an auto-redirect) that is the MS login page with the error "Your browser is set to block cookies....."
What should I change to make the HttpClient work? Or can I change the WebApi configuration to not use cookies? For the latter option I couldn't find any other alternative.
As discussed in the comments, you need to use the JWT bearer token middleware from the package Microsoft.AspNetCore.Authentication.JwtBearer.
The Open ID Connect middleware is designed to redirect a user to a sign in page, not for authenticating access tokens. An example usage of the JWT bearer token middleware can be found here: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore/blob/master/TodoListService/Startup.cs.
Take a look at this thread: https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/514 - it is showing the scenario you are trying to achieve.

How do I collect more information from a user after authenticating externally?

I'm using the Microsoft.AspNetCore.Authentication.Google package. If I allow a user to authenticate with Google, where is the place that I should inject my own form to collect some more information that Google would not have access to (a custom identifier for example).
Should I present a form, collect the data up front and store it in session or something while they go off to authorize the login?
Should I let them go authorize the login, then when the callback URL is invoked, present the form there?
There are four events exposed via the middleware:
OnTicketReceived
OnCreatingTicket
OnRedirectToAuthorizationEndpoint
OnRemoteFailure
Is there an example anywhere that this is being done? I can't seem to find anything.
I have done it with Cookie middleware. I added 'temp' cookie middleware to catch the ClaimsPrincipal from logging in to Google and then I sign in to the 'real' Cookie middleware to persist the enriched ClaimsPrincipal. The relevant piece of code in the Configure method of the StartUp class:
app.UseCookieAuthentication(
new CookieAuthenticationOptions()
{
AuthenticationScheme = "Cookie",
AutomaticAuthenticate = true,
AutomaticChallenge = true,
LoginPath = new PathString(#"/account/login"),
AccessDeniedPath = new PathString(#"/account/accessdenied")
});
app.UseCookieAuthentication(
new CookieAuthenticationOptions()
{
AuthenticationScheme = "Temp",
AutomaticAuthenticate = false
});
var googleOptions = new GoogleOptions()
{
AuthenticationScheme = "Google",
SignInScheme = "Temp",
AppId = "yourappidhere",
AppSecret = "yourappsecrethere"
};
googleOptions.Scope.Add("scopesyouneed");
app.UseGoogleAuthentication(googleOptions);
Note how the SignInScheme of the googleOptions is "Temp" and the options of the 'temp' Cookie middleware has it's AutomaticAuthenticate to false (because you don't want to automatically persist the ClaimsPrinciple in the temp Cookie, but enriched and all in the real one which is called "Cookie" here).
Then the relevant methods in my controller look like:
public async Task<IActionResult> Register(string returnUrl = null)
{
var externalPrincipal = await HttpContext.Authentication.AuthenticateAsync("Temp");
//TODO Check external principal and retrieve claims from db or whatever needs to be done here.
var claims = new List<Claim>()
{
new Claim("email", externalPrincipal.FindFirst(ClaimTypes.Email).Value)
};
var id = new ClaimsIdentity(claims, "password");
await HttpContext.Authentication.SignInAsync("Cookie", new ClaimsPrincipal(id));
await HttpContext.Authentication.SignOutAsync("Temp");
return Redirect(returnUrl);
}
public async Task<IActionResult> LogInGoogle(string returnUrl = null)
{
var queryString = !string.IsNullOrWhiteSpace(returnUrl) ? $"?returnUrl={returnUrl}" : string.Empty;
var props = new AuthenticationProperties() { RedirectUri = $#"Account/Register{queryString}" }; //new PathString(returnUrl)
return await Task.Run<ChallengeResult>(() => new ChallengeResult("Google", props));
}
Note how LoginGoogle is called via a link on your page or something. Remember how GoogleMiddleware's SignInScheme is "Temp" at this point. It's redirected to the "Register" action method. There you extract the ClaimsPrinciple from Google with the code:
var externalPrincipal = await HttpContext.Authentication.AuthenticateAsync("Temp");
At this point you can do whatever you need to do with the claims. I extract the e-mail claim as you can see. And I sign in with my "Cookie" sign in scheme, to persist the ClaimsPrinciple in a cookie. But you could also redirect to the view with the form with which you request more information from the user.

Categories

Resources