I simply want to save cookies containing security tokens, but they are not persisted in the browser. Here is my AuthController method for saving the cookie (simplified):
[AllowAnonymous]
[HttpPost("authorize")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<IActionResult> AuthorizeAsync()
{
//generating access token ommited for brevity
SetTokenCookie(accessToken);
return Ok(userIdentity);
}
And SetTokenCookie method:
private void SetTokenCookie(string accessToken)
{
var options = _jwtOptions.AccessToken;
var cookieOptions = new CookieOptions()
{
HttpOnly = true,
IsEssential = true,
Secure = false,
SameSite = SameSiteMode.Strict,
Domain = options.Issuer, //http://localhost:3394 by default
Expires = DateTime.UtcNow.AddDays(14)
};
Response.Cookies.Append(options.Name, accessToken, cookieOptions);
}
Now when I analyze the response from my Api, I see that Set-Cookie header and token itself are fine:
Decoded token:
{
"id": "70132f61-4d83-4772-9685-7a77a9204685",
"name": "admin",
"email": "xyz#xyz.pl",
"role": "Administrator",
"persist": "True",
"nbf": 1646336045,
"exp": 1646336945,
"iat": 1646336045,
"iss": "http://localhost:3394",
"aud": [
"blog.webgateway",
"blog.blogging",
"blog.comments",
"blog.users"
]
}
But when I check the cookies, nothing is saved.
I know that there are many topics related to this issue, but I already ran out of ideas I could find online:
I wiped storage in my browser for localhost
I added custom entry in my hosts file and changed cookie domain accordingly
I tried setting cookie domain = null or different path
I added Cors and allowed credentials, any methods, any headers, any origin
I tried more permissive settings in my browser
I tried changing cookie options (HttpOnly, Secure, SameSite)
I removed UseHttpsRedirection() from my Startup.cs and made sure I connet via HTTP
Nothing seems to work. I'm using Firefox 97.0.1. Do you know what else I could try?
Did you try to change the Domain to localhost?
Per my test, using `` didn't work for me, and then I found that other cookies showed they belong to domain localhost, so I use localhost instead, then I can see the newly created cookie. I test to call the api by tools in chrome, so I think it should be similar to your scenario.
public string saveCookie() {
var cookieOptions = new CookieOptions()
{
HttpOnly = true,
IsEssential = true,
Secure = false,
SameSite = SameSiteMode.Strict,
Domain = "localhost", //using https://localhost:44340/ here doesn't work
Expires = DateTime.UtcNow.AddDays(14)
};
Response.Cookies.Append("testCookie", "Cookie content", cookieOptions);
return "hello world";
}
I've finally managed to solve the issue... Here are the steps I've made:
Changed the controller method to HttpGet, so it just looks like this now:
[AllowAnonymous]
[HttpGet("authorize")] // <-- notice the difference
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<IActionResult> AuthorizeAsync()
{
//generating access token ommited for brevity
SetTokenCookie(accessToken);
return Ok(userIdentity);
}
For some reason calling a Post request directly from the browser (Firefox at least in my case) doesn't seem to work for setting the cookies, even if the response looks fine, but when I changed it to Get method and accessed in a standard way (URL) it works. I will have to double-check if Post method works from the client (JavaScript).
Apply above solution from Tiny Wang, which means changing the cookie domain to localhost. Having the full URL indeed prevent cookie from being saved.
Edit: As pointed out by SSchmid in the comments, changing method to "Get" was only a temporary workaround while using Development Tools in Firefox. For further development or production it is not recommend to keep it as a "Get" method.
I managed to get it running by using the [FromBody] Attribute.
This works for me :
[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] LoginData data)
{
var user = await userManager.FindByEmailAsync(data.Email);
if (user == null || !await userManager.CheckPasswordAsync(user, data.Password))
return Unauthorized("Invalid Authentication");
await signInManager.SignInAsync(user, data.rememberMe);
var roles = (await userManager.GetRolesAsync(user)).ToList();
var sUser = new Models.User(user, roles);
return Ok(sUser);
}
apparently if u use HTTPPOST in combination with parameters it doesnt work.
Related
Related example: https://github.com/Sustainsys/Saml2.Samples/tree/main/v2/AspNetCore
My SAML configuration is below. Basically, I think I need the AuthnRequest to have some additional elements for the IdP to accept the request as the IdP is currently returning a 400 when a SAML request comes from the application. I'm unsure if this can be accomplished with Sustainsys.Saml2 or if I should go another route.
public static void Saml(this IConfiguration config, Saml2Options options)
{
options.SPOptions.ReturnUrl = new Uri("/", UriKind.Relative);
options.SPOptions.EntityId = new EntityId("<app entity id>");
options.SPOptions.PublicOrigin = new Uri("<app base url>");
options.IdentityProviders.Add(new IdentityProvider(
new EntityId("<IdP entity id>"),
options.SPOptions)
{
Binding = Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
MetadataLocation = "<IdP metadata.xml>",
AllowUnsolicitedAuthnResponse = true,
LoadMetadata = true,
});
}
For context, similar to the example, I have controller actions for initiation:
[AllowAnonymous]
[HttpPost, HttpGet]
public IActionResult Login()
{
var props = new AuthenticationProperties
{
RedirectUri = "/",
};
return Challenge(props, Saml2Defaults.Scheme);
}
[Authorize]
[HttpPost, HttpGet]
public IActionResult Logout()
{
var props = new AuthenticationProperties
{
RedirectUri = "/"
};
return SignOut(props, Saml2Defaults.Scheme);
}
I used a SAML tracer to compare an unsuccessful SAML request/response initiated by a Challenge() in the application vs. a successful SAML request initiated by a secure path/redirect added to the shibboleth2.xml.
If I compare the GET requests to the IdP (referring to the AuthnRequest now), the destination & issuer are identical in both requests and AssertionConsumerServiceURL for the Sustainsys.Saml2 request is <app base url>/Saml2/Acs which I believe is the correct response handler.
However, I seem to be missing a few elements in AuthnRequest with Sustainsys.Saml2 like the following. For example, my successful SAML request (not using non-Sustainsys.Saml2) has a NameIDPolicy and two AuthnContextClassRef.
<samlp:NameIDPolicy AllowCreate="1"></samlp:NameIDPolicy>
<samlp:RequestedAuthnContext>
<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">...</saml:AuthnContextClassRef>
<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">...</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
The AuthnRequest tag itself also has ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" on it. Issuer is also set to <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">...</saml:Issuer> rather than simply <saml2:Issuer>...</saml2:Issuer>.
I have tried to modify various options.SPOptions settings to format the request correctly. For example, I have tried this for adding the NameIDPolicy element:
options.SPOptions.NameIdPolicy = new Saml2NameIdPolicy(allowCreate: true, NameIdFormat.Unspecified);
And for RequestedAuthnContext. Though it appears I can only add one/not multiple.
options.SPOptions.RequestedAuthnContext = new Saml2RequestedAuthnContext(
new Uri("urn:oasis:names:tc:SAML:2.0:assertion:..."),
AuthnContextComparisonType.Minimum);
Results in this inside AuthnRequest:
<saml2p:RequestedAuthnContext Comparison="minimum">
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:assertion:...</saml2:AuthnContextClassRef>
</saml2p:RequestedAuthnContext>
Ideally it would be this like the successful request:
<samlp:RequestedAuthnContext>
<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">...</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
Any ideas on how I can move forward? Would it be easier to have the secure path/shibboleth2.xml handle the SAML request and use some different method of capturing the SAML response/hooking into authentication?
Thanks!
I'm building a web api and I have a method in my controller which gives the user a cookie. I can see it in the browser it is set, everything is fine.
[HttpGet]
[Route("[controller]/cookie")]
public IActionResult Cookie()
{
string cookieName = "av225461";
string key = $"blahblah";
HttpContext.Response.Cookies.Append(
cookieName, key,
new CookieOptions() { SameSite = SameSiteMode.Unspecified, HttpOnly = true, Expires =
DateTime.UtcNow.AddMinutes(15)/*, Secure = true*/ });
return Ok("");
}
But I am not able to read it in my Get method, if I am sending a request to my controller. The Cookies.Count is 0. Even if the cookie is set in browser and inthe requestheader of Firefox.
if (Request.Cookies.Count > 0)
{
//some code here
}
With postman sometimes it worked, and sometimes not. Someone an idea?
I have front app on angular 5 and backend api on c# using identity server.
The problem is that when I click logout button, the token is removed and i am redirected to logout page.
But when I try to refresh main page, I am redirected to microsoftonline.com
authenticated automatically and redirected back to main page
I am missing providing username and password here, and this occurs in chrome incognito.
What I noticed is that if I remove manually the cookie from microsoftonline.com
and repeat the process, this time I will be asked for username and password.
So first I tried to clean all cookies this way but it din't help
foreach (var key in HttpContext.Request.Cookies.Keys)
{
HttpContext.Response.Cookies.Append(key, "", new CookieOptions() { Expires = DateTime.Now.AddDays(-1) });
}
bellow is my accountcontroller logout method and cookie screen
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutViewModel model)
{
var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
var subjectId = HttpContext.User.Identity.GetSubjectId();
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
if (model.LogoutId == null)
{
model.LogoutId = await interaction.CreateLogoutContextAsync();
}
try
{
await signInManager.SignOutAsync();
}
catch (NotSupportedException)
{
}
}
// set this so UI rendering sees an anonymous user
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await interaction.GetLogoutContextAsync(model.LogoutId);
var vm = new LoggedOutViewModel
{
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = logout?.ClientId,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
await persistedGrantService.RemoveAllGrantsAsync(subjectId, "angular2client");
return View("LoggedOut", vm);
}
If I understand correctly you are federating to Microsoft from your IdentityServer4 service? If so when you sign out of your identity service you should also give the user the option to sign out of the external provider (if it supports the relevant feature - it'd need to define an end_session_endpoint in the discovery document).
This functionality is supported by the standard OIDC middleware so you should be able to initiate signout by calling SignoutAsync() and passing the name of the scheme for the MS federated sign in.
Another option is to always send prompt=login in your external sign in requests and then check the auth_time claim you get back. That way to you force interactive sign in always and also verify when it happened.
Try cleaning the cookies from the HttpContext itself, using the extension method, provided by Identity Server, like here.
Or try this:
await HttpContext.SignOutAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme);
in your Logout controller method.
3rd option (what I have in one of my test MVC clients is):
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
public void SignoutCleanup(string sid)
{
var cp = (ClaimsPrincipal)User;
var sidClaim = cp.FindFirst("sid");
if (sidClaim != null && sidClaim.Value == sid)
{
Request.GetOwinContext().Authentication.SignOut("Cookies");
}
}
Where the Logout method is called on the button click, and the SignoutCleanup is the one that is passed to Identity Server, when registering the client as a Client.BackChannelLogoutUri (or Client.FrontChannelLogoutUri, or both, depending on your scenario).
PS: Now, in general I think that your approach is not right, but I don't know your full case, so I'm not judging you - just giving and advice.
For front-end clients (Angular, Vue, vanilla JS etc..) it is recommended to use the client-side oidc-client-js library. And here is the usage example. As I said - this is just an advice, but if you are in the very beginning of your authentication setup, I would recommend you to have a look.
UPDATE 2: If I change my controller Authorize tag from this
[Authorize]
to this
[Authorize(Roles = "Read")]
then I get the checkbox for scope selection and the ajax token request contains the correct scope and completes successfully. I still end up with a red exclamation mark however. It looks like Swagger or Swashbuckle is requiring that the roles match the scope definitions? Is it possible to use the application flow with no Roles defined when using Swashbuckle? And if so how do you get that to work? Do I have to manually set the scope in the operation filter class? If it's not possible to use Swashbuckle without listing Roles in the Authorize tag, then I need to know how to assign clients roles in IdentityServer3.
UPDATE 3
If I change the Operation Filter to something like this the scope appears, but after selecting the scope and clicking on Authorize, the page just reloads. The ajax authorization was sent successfully first. This is closer, but the authorization still doesn't stick (not sure what term to use here, but stick seems to sum it up.) How do I get the authorization to stick?
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var scopes = new List<string>() { "Read" };
if (scopes.Any())
{
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", scopes }
};
operation.security.Add(oAuthRequirements);
}
}
Original Post
I'm trying to configure Swashbuckle to allow clients to test a REST service that is protected by an OAuth2 client credentials flow. The toggle never appears on the page, should it?, but I do get a red circle with an exclamation mark telling me the resource is not protected. I'm using the nuget package Swashbuckle.Core Version 5.4.0. The answer here Enable Oauth2 client credentials flow in Swashbuckle seems to follow what I've done, and have used the AssignOAuth2SecurityRequirements class verbatim. I haven't injected any javascript and don't believe I have to since my authorization scheme is fairly standard. When I remove the Authorize key word on the Controller that method no longer has the red exclamation in the Swagger UI which I'm hoping means I'm close, but I'm not finding the missing link. Since this Flow is "application" and I only have one scope I wanted to make sure that it looks configured correctly and the clientSecret is loaded in the correct spot.
UPDATE 1
I've been able to debug the AJAX call and can see that the scope is not set and therefor not sent in the request. Why is the scope not being set? Why don't I have a checkbox to select the scope?
Here is my SwaggerConfig.cs
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "waRougeOneApp");
c.OAuth2("oauth2")
.Description("OAuth2 Client Credentials Grant Flow")
.Flow("application")
.TokenUrl("https://securitydev.rougeone.com/core/connect/token")
.Scopes(scopes =>
{
scopes.Add("Read", "Read access to protected resources");
});
c.IncludeXmlComments(GetXmlCommentsPath());
c.UseFullTypeNameInSchemaIds();
c.DescribeAllEnumsAsStrings();
c.OperationFilter<AssignOAuth2SecurityRequirements>();
})
.EnableSwaggerUi(c =>
{
c.EnableOAuth2Support(
clientId: "client_id",
clientSecret: "client_secret",
realm: "swagger-realm",
appName: "Swagger UI"
);
});
}
protected static string GetXmlCommentsPath()
{
return System.String.Format(#"{0}bin\\waRougeOne.xml", System.AppDomain.CurrentDomain.BaseDirectory);
}
}
And the AssignOAuth2SecurityRequirements class is
public class AssignOAuth2SecurityRequirements : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var authorized = apiDescription.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>();
if (!authorized.Any()) return;
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{"oauth2", Enumerable.Empty<string>()}
};
operation.security.Add(oAuthRequirements);
}
}
I've been trying to find a working example with a client credentials flow without success, so I'm not 100% sure I'll see a toggle button when everything is working correctly. In the examples for the implicit flow if you hover over the red exclamation circle you see the grant types listed, clicking on the red exclamation circle shows the options for scopes listed out in which you select one and then click authorize and it comes back with a blue exclamation.
For me I never get a checkbox to select a scope, but I've only defined one scope. What am I doing wrong? I found this while debugging the swagger ui JavaScript which seems to point to having all the data it needs?
authorizations
:
null
auths
:
Array[1]
0
:
Object
name
:
"oauth2"
type
:
"oauth2"
value
:
Object
description
:
"OAuth2 Client Credentials Grant Flow"
flow
:
"application"
scopes
:
Object
Read
:
"Read access to protected resources"
__proto__
:
Object
tokenUrl
:
"https://security.starrwarrs.com/core/connect/token"
type
:
"oauth2"
__proto__
:
Object
__proto__
:
Object
length
:
1
__proto__
:
Array[0]
Solution!!
The last part was the hardest to figure out, which I finally did with the help of the Chrome Developer tools that showed a little red X on the network tag showing the following error message:
XMLHttpRequest cannot load http://security.RogueOne.com/core/connect/token. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:62561' is therefore not allowed access.
The error message finally connected the dots below, until then the on OAuthComplete complete JavaScript function would be called, but with no token. The network tab show "This request has no response data available", but I'd see a Content-Length in the Response headers with a content-type of Json. Fiddler also showed the response which looked like (and was) well formed JSON.
I described this error here Swagger UI not parsing reponse which was due to IdentityServer3 correctly not adding a response header of "Access-Control-Allow-Origin:http://localhost:62561" You can force IdentityServer3 to send that header by updating you client creation to be the following:
new Client
{
ClientName = "SwaggerUI",
Enabled = true,
ClientId = "swaggerUI",
ClientSecrets = new List<Secret>
{
new Secret("PasswordGoesHere".Sha256())
},
Flow = Flows.ClientCredentials,
AllowClientCredentialsOnly = true,
AllowedScopes = new List<string>
{
"Read"
},
Claims = new List<Claim>
{
new Claim("client_type", "headless"),
new Claim("client_owner", "Portal"),
new Claim("app_detail", "allow")
},
PrefixClientClaims = false
// Add the AllowedCorOrigins to get the Access-Control-Allow-Origin header to be inserted for the following domains
,AllowedCorsOrigins = new List<string>
{
"http://localhost:62561/"
,"http://portaldev.RogueOne.com"
,"https://portaldev.RogueOne.com"
}
}
The AllowedCorsOrigins was the last piece of my puzzle. Hopefully this helps someone else who is facing the same issue
These are the steps which we have done and worked:
In the SwaggerConfig file, add the below settings:
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit")
.AuthorizationUrl(swaggerConfigurations["IssuerUri"].ToString())
.Scopes(scopes =>
{
scopes.Add("user_scope", "Access REST API");
});
The attributes are:
Name of the authorization scheme (oauth2 in the above sample)
Description of the authorization scheme
Flow – Type of grant
to be used
Authorization Url – Should be the Auth Url of identity management system url (eg:
https://auth2.test.com/oauth2/authorize)
Scopes – The scope name
II. In the SwaggerConfig file, add the below settings also under the swagger ui configuration section:
c.EnableOAuth2Support(swaggerConfigurations["ClientId"].ToString(), string.Empty, swaggerConfigurations["RedirectUri"].ToString(), "Swagger", " ", new Dictionary<string, string> { { "resource", GetResources() } });
The method accepts the below parameters:
clientId – This should be client ID for swagger configured in Security Token Service
clientSecret – This should be client secret key. This is required only in case of Code grant type
realm – This should be the redirect url (this should be [base address] + swagger/ui/o2c-html)
appName – This should be swagger
scopeSeperator – This is not required to be passed if there is only scope
additionalQueryStringParams – This should have the list of valid audiences and this corresponds to the resource for which the token is issued.
III. Create a new Operation Filter in the web api project as shown below:
public class CustomOperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
string clientId = "clientID";
if (apiDescription != null)
{
var actFilters = apiDescription.ActionDescriptor.GetFilterPipeline();
var allowsAnonymous = actFilters.Select(f => f.Instance).OfType<OverrideAuthorizationAttribute>().Any();
if (allowsAnonymous)
{
return; // must be an anonymous method
}
}
if (operation != null)
{
if (operation.security == null)
{
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
}
var authRequirements = new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new List<string> { clientId } }
};
operation.security.Add(authRequirements);
}
}
}
This class will be used to bind the OAuth scopes to the individual operations
IV. Add the above filter in the swagger config file (see code below)
c.OperationFilter<CustomOperationFilter>();
V. Configure the Client ID, Secret, Redirect Url and Resource in Security Token Service
VI. In the Web API project, if there is an index.html being used to inject API specific UI fields/styles, then make sure that all the javascript code is kept intact with the Swashbuckle version of the index.html file (as provided in the location - https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Core/SwaggerUi/CustomAssets/index.html)
Let me put the problem with a bit of structure.
Context
We have a web application build with Web Forms and hosted in an Azure Web App that authenticates the users against an Azure Active Directory using the OWIN + OpenId Connect standards.
The authentication process works like a charm and users are able to access the application without any problem.
So, whats the issue?
After struggling for many days with it I'm unable to pass any query string parameter to the application through the authentication process. For example, if I try to access the application for the first time through the URL: https://myapp.azurewebsites.net/Default.aspx?param=value. The reason I need to pass this parameter is that it triggers some specific actions in the main page.
The problem is that after the authentication redirects to the webapp's main page the original query string parameters of the request are gone.
The code
The startup class looks like this:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = Constants.ADTenant.ClientId,
Authority = Constants.ADTenant.Authority,
PostLogoutRedirectUri = Constants.ADTenant.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(Constants.ADTenant.ClientId,
Constants.ADTenant.AppKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
Constants.ADTenant.ObjectIdClaimType).Value;
AuthenticationContext authContext = new AuthenticationContext(Constants.ADTenant.Authority,
new NaiveSessionCache(userObjectID));
if (HttpContext.Current != null)
{
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential,
Constants.ADTenant.GraphResourceId);
AuthenticationHelper.token = result.AccessToken;
AuthenticationHelper.refreshToken = result.RefreshToken;
}
return Task.FromResult(0);
}
}
});
And it works properly!
What I already tried
I've got access to the original request Url by adding an overwrite of the RedirectToIdentityProvider notification:
RedirectToIdentityProvider = (context) =>
{
// Ensure the URI is picked up dynamically from the request;
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.RedirectUri = appBaseUrl;
return Task.FromResult(0);
}
With this I tried to force the redirect to the main page including the original query string parameter, but then the redirection after authentication breaks and gets stuck in an infinite loop.
I've also tried with changing the redirect url of the application configuration in Azure AD without luck. Also tried to store the query string parameters somewhere else, but the Session is not accessible that early in the process.
Does anyone know what am I doing wrong? Or I'm just asking for something impossible? Any help would be appreciated.
Thank you very much in advance!
I recently had a need to do the exact same thing. My solution may not be the most sophisticated, but simple isn't always bad either.
I have two Authentication Filters...
The first filter is applied to all controllers that could potentially be hit with query string parameters prior to authorization. It checks if the principal is authenticated. If false it caches the complete url string in a cookie. If true it looks for any cookies present and clears them, just for cleanup.
public class AuthCheckActionFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if (!filterContext.Principal.Identity.IsAuthenticated)
{
HttpCookie cookie = new HttpCookie("OnAuthenticateAction");
cookie.Value = filterContext.HttpContext.Request.Url.OriginalString;
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
else
{
if (filterContext.HttpContext.Request.Cookies.AllKeys.Contains("OnAuthenticateAction"))
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["OnAuthenticateAction"];
cookie.Expires = DateTime.Now.AddDays(-1);
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}
The second filter is applied only to the default landing page, or in other words where the identity server is redirecting after login. This second filter looks for a cookie and if it exists it calls response.Redirect on cookie value.
public class AutoRedirectFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if(filterContext.Principal.Identity.IsAuthenticated)
{
if(filterContext.HttpContext.Request.Cookies.AllKeys.Contains("OnAuthenticateAction"))
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["OnAuthenticateAction"];
filterContext.HttpContext.Response.Redirect(cookie.Value);
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}