I have a WebAPI with OAuth login configured like this:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = "https://www.microsoft.com/",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
});
and Login enforced for all Controllers using
config.Filters.Add(new System.Web.Http.AuthorizeAttribute());
I now want to add an ApiController called LogoutController (guess what it does).
I have found that I can logout from MVC using
System.Web.Security.FormsAuthentication.SignOut();
but I am not logged out from WebAPI that way. I have not found any information how to logout from WebAPI. But I have found that there may be a bug in logout procedure, the cookie is kept and has to be removed manually, but then, the code is MVC again, and it seems as if I can't get a HttpCookie into my HttpResponseMessage object:
[HttpGet]
public HttpResponseMessage Logout()
{
FormsAuthentication.SignOut();
// clear authentication cookie
HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
cookie1.Expires = DateTime.Now.AddYears(-1);
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent("<html><title>Logout successful</title><body style=\"font-family:sans-serif\"><div style=\"display:table; width:100%; height:100%; margin:0; padding:0; \"><div style=\"display:table-cell; vertical-align:middle; text-align:center;\">You have been successfully logged out.<br>You can close this window/tab now.</div></div></body></html>");
response.Headers.AddCookies(cookie1); // Types don't match
return response;
}
How can I achieve that my WebAPI is logged out and does require OAuth to be done again before I am logged in?
You can't logout of the API because you're not logged in to it!
For example, say your API uses Facebook as its OpenID authentication provider.
Your user will have to log into facebook to use your API. Your API will redirect them to facebook auth server and if they are not logged in - facebook will ask them to log in.
If the user decides to stay logged into facebook, then each time they use your API, they will not be required to login to facebook again and your middleware code will obtain a valid token for them to access your API.
Your API can't remove the browser cookie between facebook and your user's browser so you can't log them out of facebook, so you can't stop them getting new tokens when they want.
I don't know what OpenID provider you use but I would think the above applies for any.
You can log out of MVC app as it would have created a cookie between you (user agent) and the MVC app when you logged in. It can delete its own cookie!
The easiest way is for the client itself to just "forget" the token - no need to tell server about it (this is what clearing the auth cookie really is doing - making the browser remove the cookie).
If you want the token itself to be no longer valid, than you would need to maintain a list of revoked tokens. For various reasons you may want your access tokens to be always valid but short lived and revoke refresh tokens instead.
Related
I've stumbled upon such code multiple times (e.g. in some controller actions):
var result = await HttpContext.AuthenticateAsync();
if (result.Succeeded)
{
// Get authenticated user's principal
var user = result.Principal;
}
What is the benefit of calling AuthenticateAsync() directly if we can access current user this way, after the authentication middleware validated the cookie on request and signed user in?
if (HttpContext.User.Identity.IsAuthenticated)
{
// Get authenticated user's principal
var user = HttpContext.User;
}
An authentication scheme's authenticate action is responsible for constructing the user's identity based on request context. It returns an AuthenticateResult indicating whether authentication was successful and, if so, the user's identity in an authentication ticket.
You could check this doucment related,and the description of authenticateasync method
I'm struggling to implement a custom auth flow with OAuth and JWT.
Basically it should go as follows:
User clicks in login
User is redirect to 3rd party OAuth login page
User logins into the page
I get the access_token and request for User Info
I get the user info and create my own JWT Token to be sent back and forth
I have been following this great tutorial on how to build an OAuth authentication, the only part that differs is that Jerrie is using Cookies.
What I Have done so far:
Configured the AuthenticationService
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "3rdPartyOAuth";
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie() // Added only because of the DefaultSignInScheme
.AddJwtBearer(options =>
{
options.TokenValidationParameters = // Ommited for brevity
})
.AddOAuth("3rdPartyOAuth", options =>
{
options.ClientId = securityConfig.ClientId;
options.ClientSecret = securityConfig.ClientSecret;
options.CallbackPath = new PathString("/auth/oauthCallback");
options.AuthorizationEndpoint = securityConfig.AuthorizationEndpoint;
options.TokenEndpoint = securityConfig.TokenEndpoint;
options.UserInformationEndpoint = securityConfig.UserInfoEndpoint;
// Only this for testing for now
options.ClaimActions.MapJsonKey("sub", "sub");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
// Request for user information
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
Auth Controller
[AllowAnonymous]
[HttpGet("login")]
public IActionResult LoginIam(string returnUrl = "/auth/loginCallback")
{
return Challenge(new AuthenticationProperties() {RedirectUri = returnUrl});
}
[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
// Here is where I expect to get the user info, create my JWT and send it back to the client
return Ok();
}
Disclaimer: This OAuth flow is being incorporated now. I have a flow for creating and using my own JWT working and everything. I will not post here because my problem is before that.
What I want
In Jerrie's post you can see that he sets DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;. With that, when the /auth/loginCallback is reached I have the user claims in the HttpContext.
The problem is my DefaultAuthenticateScheme is set to JwtBearersDefault and when the loginCallback is called I can't see the user claims nowhere in the Request.
How can I have access to the information gained on the OnCreatingTicketEvent in my callback in this scenario?
Bonus question: I don't know much about OAuth (sure that is clear now). You may have noted that my options.CallbackPath differs from the RedirectUri passed in the Challenge at the login endpoint. I expected the option.CallbackPath to be called by the 3rd Part OAuth provider but this is not what happens (apparently). I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right? The Callback is used for nothing but a match configuration? I can even comment the endpoint CallbackPath points to and it keep working the same way...
Thanks!
Auth
As Jerrie linked in his post, there is a great explanation about auth middlewares:
https://digitalmccullough.com/posts/aspnetcore-auth-system-demystified.html
You can see a flowchart in the section Authentication and Authorization Flow
The second step is Authentication middleware calls Default Handler's Authenticate.
As your default auth handler is Jwt, the context is not pupulated with the user data after the oauth flow,
since it uses the CookieAuthenticationDefaults.AuthenticationScheme
Try:
[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
//
// Read external identity from the temporary cookie
//
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("Nein");
}
var oauthUser = result.Principal;
...
return Ok();
}
Great schemes summary: ASP.NET Core 2 AuthenticationSchemes
You can persist your user with
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signinasync?view=aspnetcore-2.2
Bonus
I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right?"
Yes.
For security reasons, the registered callback uri (on authorization server) and the provided callback uri (sent by the client) MUST match.
So you cannot change it randomly, or if you change it, you have to change it on the auth server too.
If this restriction was not present, f.e. an email with a mailformed link (with modified callback url) could obtain grant.
This is called Open Redirect, the rfc refers to it too: https://www.rfc-editor.org/rfc/rfc6749#section-10.15
OWASP has a great description: https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md
I can even comment the endpoint CallbackPath points to and it keep working the same way..."
That is because your client is trusted (you provide your secret, and you are not a fully-frontend Single Page App). So it is optional for you to send the callback uri.
But IF you send it, it MUST match with the one registered on the server. If you don't send it, the auth server will redirect to the url, that is registered on its side.
https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
redirect_uri
OPTIONAL. As described in Section 3.1.2.
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2
The authorization server redirects the user-agent to the
client's redirection endpoint previously established with the
authorization server during the client registration process or when
making the authorization request.
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.2
The authorization server MUST require the following clients to register their redirection endpoint:
Public clients.
Confidential clients utilizing the implicit grant type.
Your client is confidential and uses authorization code grant type (https://www.rfc-editor.org/rfc/rfc6749#section-1.3.1)
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3
If multiple redirection URIs have been registered, if only part of
the redirection URI has been registered, or if no redirection URI has
been registered, the client MUST include a redirection URI with the
authorization request using the "redirect_uri" request parameter.
You have registered your redirect uri, that's why the auth server does not raise an error.
change [AllowAnonymous]
to [Authorize]
on the 'loginCallback' endpoint (AuthController.IamCallback method)
I have an MVC site that allows logging in using both Forms login and Windows Authentication. I use a custom MembershipProvider that authenticated the users against Active Directory, the System.Web.Helpers AntiForgery class for CSRF protection, and Owin cookie authentication middle-ware.
During login, once a user has passed authentication against Active Directory, I do the following:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
var identity = new ClaimsIdentity(StringConstants.ApplicationCookie,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if(HttpContext.Current.User.Identity is WindowsIdentity)
{
identity.AddClaims(((WindowsIdentity)HttpContext.Current.User.Identity).Claims);
}
else
{
identity.AddClaim(new Claim(ClaimTypes.Name, userData.Name));
}
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userData.userGuid));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
My SignOut function looks like this:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
Logging in is performed via a jQuery.ajax request. On success, the Window.location is updated to the site's main page.
Logging in with both Forms and IntegratedWindowsAuthentication (IWA) works, but I've run into a problem when logging in with IWA. This is what happens:
The user selects IWA on the login page and hits the submit button. This is sent to the regular login action via an ajax request.
The site receives the request, sees the "use IWA" option and redirects to the relevant action. 302 response is sent.
The browser automatically handles the 302 response and calls the redirect target.
A filter sees that the request is headed to the IWA login action and that User.Identity.IsAuthenticated == false. 401 response is sent.
The browser automatically handles the 401 response. If the user has not authenticated using IWA in the browser yet, they get a popup to do so (default browser behavior). Once credentials have been received, the browser performs the same request with user credentials.
The site receives the authenticated request and impersonates the user to perform a check against Active Directory. If the user passes authentication, we finalize SignIn using the code above.
User is forwarded to the site's main page.
The site receives the request to load the main page. This is where things sometimes go awry.
The User.Identity at this point is of type WindowsIdentity with AuthenticationType set to Negotiate, and NOT as I would expect, the ClaimsIdentity created in the SignIn method above.
The site prepares the main page for the user by calling #AntiForgery.GetHtml() in the view. This is done to create a new AntiForgery token with the logged in user's details. The token is created with the WindowsIdentity
As the main page loads, ajax requests made to the server arrive with ClaimsIdentity! The first POST request to arrive therefore inevitably causes an AntiForgeryException where the anti-forgery token it sent is "for a different user".
Refreshing the page causes the main page to load with ClaimsIdentity and allows POST requests to function.
Second, related, problem: At any point after the refresh, once things are supposedly working properly, a POST request may arrive with WindowsIdentity and not with ClaimsIdentity, once again throwing an AntiForgeryException.
It is not any specific post request,
it is not after any specific amount of time (may be the first/second request, may be the hundredth),
it is not necessarily the first time that specific post request got called during that session.
I feel like I'm either missing something regarding the User.Identity or that I did something wrong in the log-in process... Any ideas?
Note: Setting AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; allows the AntiForgery.Validate action to succeed whether WindowsIdentity or ClaimsIdentity are received, but as is stated on MSDN:
Use caution when setting this value. Using it improperly can open
security vulnerabilities in the application.
With no more explanation than that, I don't know what security vulnerabilities are actually being opened here, and am therefore loathe to use this as a solution.
Turns out the problem was the ClaimsPrincipal support multiple identities. If you are in a situation where you have multiple identities, it chooses one on its own. I don't know what determines the order of the identities in the IEnumerable but whatever it is, it apparently does necessarily result in a constant order over the life-cycle of a user's session.
As mentioned in the asp.net/Security git's Issues section, NTLM and cookie authentication #1467:
Identities contains both, the windows identity and the cookie identity.
and
It looks like with ClaimsPrincipals you can set a static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> called PrimaryIdentitySelector which you can use in order to select the primary identity to work with.
To do this, create a static method with the signature:
static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)
This method will be used to go over the list of ClaimsIdentitys and select the one that you prefer.
Then, in your Global.asax.cs set this method as the PrimaryIdentitySelector, like so:
System.Security.Claims.ClaimsPrincipal.PrimaryIdentitySelector = MyPrimaryIdentitySelectorFunc;
My PrimaryIdentitySelector method ended up looking like this:
public static ClaimsIdentity PrimaryIdentitySelector(IEnumerable<ClaimsIdentity> identities)
{
//check for null (the default PIS also does this)
if (identities == null) throw new ArgumentNullException(nameof(identities));
//if there is only one, there is no need to check further
if (identities.Count() == 1) return identities.First();
//Prefer my cookie identity. I can recognize it by the IdentityProvider
//claim. This doesn't need to be a unique value, simply one that I know
//belongs to the cookie identity I created. AntiForgery will use this
//identity in the anti-CSRF check.
var primaryIdentity = identities.FirstOrDefault(identity => {
return identity.Claims.FirstOrDefault(c => {
return c.Type.Equals(StringConstants.ClaimTypes_IdentityProvider, StringComparison.Ordinal) &&
c.Value == StringConstants.Claim_IdentityProvider;
}) != null;
});
//if none found, default to the first identity
if (primaryIdentity == null) return identities.First();
return primaryIdentity;
}
[Edit]
Now, this turned out to not be enough, as the PrimaryIdentitySelector doesn't seem to run when there is only one Identity in the Identities list. This caused problems in the login page where sometimes the browser would pass a WindowsIdentity when loading the page but not pass it on the login request {exasperated sigh}. To solve this I ended up creating a ClaimsIdentity for the login page, then manually overwriting the the thread's Principal, as described in this SO question.
This creates a problem with Windows Authentication as OnAuthenticate will not send a 401 to request Windows Identity. To solve this you must sign out the Login identity. If the login fails, make sure to recreate the Login user. (You may also need to recreate a CSRF token)
I'm not sure if this will help, but this is how I've fixed this problem for me.
When I added Windows Authentication, it fluctuated between Windows and Claims identities. What I noticed is that GET requests get ClaimsIdentity but POST requests gets WindowsIdentity. It was very frustrating, and I've decided to debug and put a breakpoint to DefaultHttpContext.set_User. IISMiddleware was setting the User property, then I've noticed that it has an AutomaticAuthentication, default is true, that sets the User property. I changed that to false, so HttpContext.User became ClaimsPrincipal all the time, hurray.
Now my problem become how I can use Windows Authentication. Luckily, even if I set AutomaticAuthentication to false, IISMiddleware updates HttpContext.Features with the WindowsPrincipal, so var windowsUser = HttpContext.Features.Get<WindowsPrincipal>(); returns the Windows User on my SSO page.
Everything is working fine, and without a hitch, there are no fluctuations, no nothing. Forms base and Windows Authentication works together.
services.Configure<IISOptions>(opts =>
{
opts.AutomaticAuthentication = false;
});
I am using ASP.Net 5 MVC 6 with JWT tokens that are created while the user is on another site which this site is a subdomain of. My goal is to pass the token along with the request to this subdomain. If a user happens to try to come to this subdomain url without the proper token in the header then I want to redirect them to the main site login page.
After much frustration with the newest RC-1 release and using JWT tokens with a SecureKey instead of certificates. I finally got my code working by using the RC-2 nightly build version. Now my problem is that I want to be able to redirect to an outside url in the case of unauthenticated users. Here is an example of my authentication code:
var key = "mysupersecretkey=";
var encodedkey2 = Convert.FromBase64String(key);
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(encodedkey2);
options.TokenValidationParameters.ValidIssuer = "https://tv.alsdkfalsdkf.com/xxx/yyy";
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ValidAudience = "https://www.sdgfllfsdkgh.com/";
options.TokenValidationParameters.ValidateAudience = true;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = "https://tv.sdfkaysdf.com/xxx/yyy"
};
});
now I see other examples which are using OpedId and they have it pretty easy , there is a parameter called RedirectUrl
app.UseOpenIdConnectAuthentication(options => {
...
options.RedirectUri = "https://localhost:44300";
...
});
any idea how to set RedirectUrl when using JwtBearerAuthentication ???
There's no such property for a simple reason: the JWT bearer middleware (like the more generic OAuth2 middleware in Katana) has been designed for API authentication, not for interactive authentication. Trying to trigger a redirection in this case wouldn't make much sense for headless HTTP clients.
That said, it doesn't mean that you can't redirect your unauthenticated users at all, at some point. The best way to handle that is to catch the 401 response returned by the JWT middleware at the client level and redirect the user to the appropriate login page. In JS applications for instance, this is usually done using an HTTP interceptor.
If you're really convinced breaking the OAuth2 bearer specification is the right thing to do, you can do that using the OnChallenge notification:
app.UseJwtBearerAuthentication(options => {
options.Events = new JwtBearerEvents {
OnChallenge = context => {
context.Response.Redirect("http://localhost:54540/login");
context.HandleResponse();
return Task.FromResult(0);
}
};
});
I have an MVC 5 web app. Currently the application is using Individual Accounts to login. There are also roles in place for authorization. The users login using their username and password.
The problem is like this. I need to authenticate and authorize users requests that are coming via a reverse proxy.
User make's a request to for the app (reverse proxy url) http://myapp.domain.com
Reverse proxy will make additional calls and verify that the user is authorized to access the app content. (missing from diagram for simplicity).
If everything ok with the user the call is redirected to the actual MVC application where the request will contain a header with the username.
The MVC must check that the request is coming from the reverse proxy (not a problem to do that, IP check, plus some key sent as headers.)
MVC should read the request header, get the username, authenticate it and authorize it based on the role or roles he has.
Deny the request if the request doesn't come from the reverse proxy, deny the request if the user doesn't have appropriate roles. Example user is in role Visitor but he's trying to access admin role content.
My problem is in the authentication and authorization of the request when there is present just the username.
As the application uses already username and password for authentication, I'm thinking to do the following:
The reverse proxy will send the request to https://realapp.domain.com/account/login.
In the action login from account controller I can implement the logic to read the request and get the username from the header
At this point we know that user X is authenticated because the reverse proxy and additional systems will check that. So basically all requests arriving are considered safe (from the reverse proxy server)
If the username doesn't exists within the database (first time call to the application) the MVC app will create the user with a dummy password (Password123).
If the username exists within the database then log him in using the username and the dummy password Password123 var result = await SignInManager.PasswordSignInAsync("username", "Password123", false, shouldLockout: false);
User is authenticated and authorized.
Basically my idea is to set for each user same password in order to authenticate them within the application and make use of roles.
What do you think?
Since Identity is claim based. You don't need any password or even any user object to authenticate users. So using storage in Identity is totally optional also. You just need create some claims and authorize your users based on those. Consider this simple example as a clue:
// imaging this action is called by proxy
public ActionResoult Login()
{
// this custom method extract username from header and check IP and more
var username=_myUserManager.GetUserName();
if(username!=null)
{
// optionally you have own user manager which returns roles from username
// no matter how you store users and roles
string[] roles=_myUserManager.GetUserRoles(username);
// user is valid, going to authenticate user for my App
var ident = new ClaimsIdentity(
new[]
{
// adding following 2 claim just for supporting default antiforgery provider
new Claim(ClaimTypes.NameIdentifier, username),
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"),
// an optional claim you could omit this
new Claim(ClaimTypes.Name, username),
// populate assigned user's role form your DB
// and add each one as a claim
new Claim(ClaimTypes.Role, roles[0]),
new Claim(ClaimTypes.Role, roles[1]),
// and so on
},
DefaultAuthenticationTypes.ApplicationCookie);
// Identity is sign in user based on claim don't matter
// how you generated it
HttpContext.GetOwinContext().Authentication.SignIn(
new AuthenticationProperties { IsPersistent = false }, ident);
// auth is succeed, without needing any password just claim based
return RedirectToAction("MyAction");
}
// invalid user
ModelState.AddModelError("", "We could not authorize you :(");
return View();
}
Now we authorized our users without any password so we could use Authorize filter as well:
[Authorize]
public ActionResult Foo()
{
}
// since we injected user roles to Identity we could do this as well
[Authorize(Roles="admin")]
public ActionResult Foo()
{
// since we injected our authentication mechanism to Identity pipeline
// we have access current user principal by calling also
// HttpContext.User
}
Note: Your proxy must handle your apps generated cookies and deliver them to your users properly. Since your app based on cookie.
You could download Token Based Authentication Sample from my Github repo and also read my other answer which is closes to your scenario.