I am new to .net core. We are trying to add auth to a project.
My HandleAuthenticateAsync function of Authentication handler looks like this.
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = Gettoken(_httpContextAccessor.HttpContext.Request);
if(token == null)
{
return AuthenticateResult.NoResult();
}
if(Validatetoken(token) == false)
{
return AuthenticateResult.Fail("invalid token");
}
var tokenIdentity = new TokenIdentity(token);
var principal = new GenericPrincipal(tokenIdentity, null);
var authTicket = new AuthenticationTicket(principal, "schema");
return AuthenticateResult.Success(authTicket);
}
I want to throw client side error only when the token is invalid(on AuthenticateResult.Fail)
and want to proceed with processing the request in other cases(on AuthenticateResult.NoResult and AuthenticateResult.Success)
How can I achieve this requirement? Thanks.
You can use authorization filters or action filter to handle this requirement.
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0#authorization-filters
Related
I've managed to configure my application to authenticate using ADB2C, and it seems to work fine. The ADB2C code implemented is a tweak of one of Microsoft's samples, in which they use a SessionTokenCache class to manage instances of TokenCache. In my application, I retrieve the access token as follows:
private async Task<string> _getAccessToken(IConfidentialClientCredentials credentials)
{
if (this.HasCredentials())
{
var clientCredential = new ClientCredential(credentials.ClientSecret);
var userId = this._getUserIdClaimValue();
var tokenCache = new SessionTokenCache(_httpContextResolver.Context, userId);
var confidentialClientApplication = new ConfidentialClientApplication(
credentials.ClientId,
credentials.Authority,
credentials.RedirectUri,
clientCredential,
tokenCache.GetInstance(),
null);
IAccount account = confidentialClientApplication.GetAccountsAsync().Result.FirstOrDefault();
if (account == null)
{
return "";
}
var authenticationResult = await confidentialClientApplication.AcquireTokenSilentAsync(
credentials.ApiScopes.Split(' '),
account,
credentials.Authority,
false);
return authenticationResult.AccessToken;
}
else
{
return "";
}
}
This method is used to get the access token and pass it in the request header of an HttpClient as follows:
...
using (var request = new HttpRequestMessage(HttpMethod.Get, address.AbsoluteUri))
{
if (this.HasCredentials())
{
string accessToken = await this._getAccessToken(_confidentialClientCredentials);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
using (HttpResponseMessage response = await this.SendAsync(request))
{
//result-processing logic
}
...
The problem is that when the app is restarted, the user remains authenticated through the ADB2C cookie, but confidentialClientApplication.GetAccountsAsync().Result.FirstOrDefault(); returns null. This probably happens because the token cache is destroyed on app restart, so I can probably use a Redis cache to fix.
My main issue however is how to handle the situation of having a null Account but being "authenticated" at the same time. How are my requests to my website being authenticated even though I have a null Account? Shouldn't it fail and redirect me to login page, for example?
I tried looking into Authorization filters, and using the following code to hook up to the auth process and validate if user is null there, but to no avail. The following events are not being called ever (this is in ConfigureServices):
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("ActiveDirectoryB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("ActiveDirectoryB2C", options))
.AddCookie((options) => new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
// context.Principal gives you access to the logged-in user
// context.Properties.GetTokens() gives you access to all the tokens
return Task.CompletedTask;
},
OnSignedIn = context =>
{
return Task.CompletedTask;
}
}
});
It all feels a bit too abstracted for me to make any sense of what's going on. Either that, or I'm missing something fundamental.
Note: The error "Null user was passed in AcquiretokenSilent API. Pass in a user object or call acquireToken authenticate.
" is thrown if I try to pass the null account to the confidentialClientApplication.AcquireTokenSilentAsync() method.
I solved with this code:
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.Exception is Microsoft.Identity.Client.MsalUiRequiredException)
{
RedirectToAction("SignIn", "Account");
}
else {
//Do your logging
// ...
}
}
I'll search for a better solution.
I have an Asp.net web api, which is configured with OAuth. Now I have new client who cannot use Oauth but wants to use Basic Authentication with the same endpoint url.
Haven't found any ways to do this yet. Any help on this is appreciated. Thanks in Advance
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if ((Thread.CurrentPrincipal.Identity.Name?.Length ?? 0) <= 0)
{
AuthenticationHeaderValue auth = actionContext.Request.Headers.Authorization;
if (string.Compare(auth.Scheme, "Basic", StringComparison.OrdinalIgnoreCase) == 0)
{
string credentials = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(auth.Parameter));
int separatorIndex = credentials.IndexOf(':');
if (separatorIndex >= 0)
{
string userName = credentials.Substring(0, separatorIndex);
string password = credentials.Substring(separatorIndex + 1);
var userManager = new MembershipUserManager();
var user = userManager.FindAsync(userName, password).Result;
if (user != null)
Thread.CurrentPrincipal = actionContext.ControllerContext.RequestContext.Principal = new GenericPrincipal(new GenericIdentity(userName, "Basic"), System.Web.Security.Roles.Provider.GetRolesForUser(userName));
}
}
}
return base.IsAuthorized(actionContext);
}
}
Use this code once you have set up the token auth (Oauth) and this would work for both: This attribute should be used everywhere (ditch the Authorize) [contains roles] and would verify the Basic auth, whereas the base.IsAuthorized(actionContext); would verify the token approach (Oauth).
MembershipUserManager is a custom class I've created to make this work with Membership, I'm guessing you'd use Identity User Manager.
This question is continuation of my previous one: ASP.Net Identity 2 login using password from SMS - not using two-factor authentication
I've build my custom OAuthAuthorizationServerProvider to support custom grant_type.
My idea was to create grant_type of sms that will allow user to generate one-time access code that will be send to his mobile phone and then user as password when sending request with grant_type of password.
Now after generating, storing and sending via SMS that password I'd like to return custom response, not token from my GrantCustomExtension.
public override async Task GrantCustomExtension(OAuthGrantCustomExtensionContext context)
{
const string allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] {allowedOrigin});
if (context.GrantType != "sms")
{
context.SetError("invalid_grant", "unsupported grant_type");
return;
}
var userName = context.Parameters.Get("username");
if (userName == null)
{
context.SetError("invalid_grant", "username is required");
return;
}
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
context.SetError("invalid_grant", "user not found");
return;
}
var generator = new TotpSecurityStampBasedTokenProvider<ApplicationUser, string>();
await userManager.UpdateSecurityStampAsync(user.Id);
var accessCode = await generator.GenerateAsync("SMS", userManager, user);
var accessCodeExpirationTime = TimeSpan.FromMinutes(5);
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
//return 200 (OK)
//with content type="application/json; charset=utf-8"
//and custom json content {"message":"code send","expires_in":300}
//skip part below
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, "SMS");
var ticket = new AuthenticationTicket(oAuthIdentity, null);
context.Validated(ticket);
}
How can I stop generating token and return custom response from OAuthAuthorizationServerProvider?
I'm aware of two methods: TokenEndpoint, TokenEndpointResponse, but I'd like to override whole response, not just token.
EDIT:
For now I'm creating temporary ClaimsIdentity in GrantCustomExtension using code below:
var ci = new ClaimsIdentity();
ci.AddClaim(new Claim("message","send"));
ci.AddClaim(new Claim("expires_in", accessCodeExpirationTime.TotalSeconds.ToString(CultureInfo.InvariantCulture)));
context.Validated(ci);
and I'm overriding TokenEndpointResponse:
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
if (context.TokenEndpointRequest.GrantType != "sms") return base.TokenEndpointResponse(context);
//clear response containing temporary token.
HttpContext.Current.Response.SuppressContent = true;
return Task.FromResult<object>(null);
}
This has two issues: when calling context.Validated(ci); I'm saying this is a valid user, but instead I'd like to response information that I've send access code via SMS.
HttpContext.Current.Response.SuppressContent = true; clears response, but I'd like to return something instead of empty response.
This is more of a workaround then a final solution, but I believe it is the most reliable way of solving your issue without rewriting tons of code from the default OAuthAuthorizationServerProvider implementation.
The approach is simple: use a Owin middleware to catch token requests, and overwrite the response if an SMS was sent.
[Edit after comments] Fixed the code to allow the response body to be buffered and changed as per this answer https://stackoverflow.com/a/36414238/965722
Inside your Startup.cs file:
public void Configuration(IAppBuilder app)
{
var tokenPath = new PathString("/Token"); //the same path defined in OAuthOptions.TokenEndpointPath
app.Use(async (c, n) =>
{
//check if the request was for the token endpoint
if (c.Request.Path == tokenPath)
{
var buffer = new MemoryStream();
var body = c.Response.Body;
c.Response.Body = buffer; // we'll buffer the response, so we may change it if needed
await n.Invoke(); //invoke next middleware (auth)
//check if we sent a SMS
if (c.Get<bool>("sms_grant:sent"))
{
var json = JsonConvert.SerializeObject(
new
{
message = "code send",
expires_in = 300
});
var bytes = Encoding.UTF8.GetBytes(json);
buffer.SetLength(0); //change the buffer
buffer.Write(bytes, 0, bytes.Length);
//override the response headers
c.Response.StatusCode = 200;
c.Response.ContentType = "application/json";
c.Response.ContentLength = bytes.Length;
}
buffer.Position = 0; //reset position
await buffer.CopyToAsync(body); //copy to real response stream
c.Response.Body = body; //set again real stream to response body
}
else
{
await n.Invoke(); //normal behavior
}
});
//other owin middlewares in the pipeline
//ConfigureAuth(app);
//app.UseWebApi( .. );
}
And inside your custom grant method:
// ...
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
context.OwinContext.Set("sms_grant:sent", true);
//you may validate the user or set an error, doesn't matter anymore
//it will be overwritten
//...
I would recommend to have a look at this answer :
https://stackoverflow.com/a/24090287/2508268
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
I have a Web API 2 project and we are using basic authorization. The client sends the username:password in the authorization header - we extract it and connect to an LDAP server to
verify the user
verify their password, and then
get an attribute value from their LDAP response and pass it into the
IPrincipal of the request.
The issue is, when I access the IPrincipal in the controller via System.Web.Http.ApiController.User.Identity.Name I am seeing that the value passed in is not always correct!
Example:
In either a message handler or a filter, I set the IPrincipal as so:
public class BasicAuthenticationAttribute : Attribute, IAuthenticationFilter
{
...other code...
private IPrincipal ReturnPrincipal(string UserID, List<string> roles)
{
roles.Add("SomeRole");
// UserID = 7144 or 8899 (load testing)
var identity = new GenericIdentity(UserID, "Basic");
var principal = new GenericPrincipal(identity, roles.ToArray());
return principal;
}
}
Edit: Here is how we are setting the principal in the filter:
var principal = await AuthenticateAsync(UserID, password, cancellationToken);
if (principal == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
else
{
// Authentication was attempted and succeeded. Set Principal to the authenticated user.
context.Principal = principal;
}
Later in the lifecycle of the request, in the controller, I try to access this value like so:
protected bool IsAuthorized(string UserID)
{
return User.Identity.Name == UserID;
}
So what happens here is I'm getting the User.Identity.Name from ApiController.User.Identity.Name, under load testing, I send 2 threaded parallel requests at the SAME TIME - when I do this, the Request object shows the correct ID in the URL, however, the IPrincipal that is set in the filter is NOT right. This is a fixed test with expected results, so this should never happen. If it stagger the calls, the issue does not come up! If the calls are sent at the same time, the issue occurs 100% of the time.
Here is how I am calling the service:
var task1 = Task.Run(() => CallServerInParallel(requests2, api));
var task2 = Task.Run(() => CallServerInParallel2(requests2, api));
Task.WaitAll(task1, task2);
...
private static void CallServerInParallel2(List<int> requests2, string api)
{
Parallel.ForEach(requests2, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, async p =>
{
var client = new RemoteClient(api);
client.AddAuthHeader("d2JsMTE4NTgzOUBbdGVhY2NlcHQuY29tOnRlc3QxMjM0");
var response = await client.Get<dynamic>("CustomerSite/5537");
});
}
private static void CallServerInParallel2(List<int> requests2, string api)
{
Parallel.ForEach(requests2, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, async p =>
{
var client = new RemoteClient(api);
client.AddAuthHeader("d2JsMTE4NTgzOUBkdGVhY1NlcHQuY29tOnRlc3QxMjM0");
var response = await client.Get<dynamic>("CustomerSite/5538");
});
}
How can I access the correct IPrincipal.User.Identity.Name for the request?
As referenced here : Setting the Principal
I got around this kind of issue by making sure that in my filter after setting the context principal that I also do the following
If your application performs any custom authentication logic, you must
set the principal on two places:
Thread.CurrentPrincipal. This property is the standard way to set the thread's principal in .NET.
HttpContext.Current.User. This property is specific to ASP.NET.
The following code shows how to set the principal:
private void SetPrincipal(IPrincipal principal)
{
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
}
For web-hosting, you must set the principal in both places; otherwise
the security context may become inconsistent. For self-hosting,
however, HttpContext.Current is null. To ensure your code is
host-agnostic, therefore, check for null before assigning to
HttpContext.Current, as shown.
var principal = await AuthenticateAsync(UserID, password, cancellationToken);
if (principal == null) {
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
} else {
// Authentication was attempted and succeeded. Set Principal to the authenticated user.
context.Principal = principal;
SetPrincipal(principal);
}
I use dotnetOpenAuth. I want to request authorization to the user's gamil.
Do I need to use openId first?
Cannot find a decent tutorail. Can anyone help?
Tried this code unsuccesfully. Anyway I don't seems to ask for Gmail scope at the auth request, so I'm confused
public void PrepareAuthorizationRequest(Uri authCallbakUrl)
{
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// request access
consumer.Channel.Send(consumer.PrepareRequestUserAuthorization(authCallbakUrl, null, null));
throw new NoRedirectToAuthPageException();
}
public ProcessAuthorizationRequestResponse ProcessAuthorizationRequest()
{
ProcessAuthorizationRequestResponse response;
// Process result from the service provider
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
var accessTokenResponse = consumer.ProcessUserAuthorization();
// If we didn't have an access token response, this wasn't called by the service provider
if (accessTokenResponse == null)
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = false
};
else
{
// Extract the access token
string accessToken = accessTokenResponse.AccessToken;
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = true,
Token = accessToken,
Secret = mConsumerTokenManager.GetTokenSecret(accessToken)
};
}
return response;
}
private string Test2()
{
// Process result from linked in
var google = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// var accessToken = GetAccessTokenForUser();
var accessToken = String.Empty;
// Retrieve the user's profile information
var endpoint = GoogleConsumerConsts.GetGmailFeedsEndpoint;// new MessageReceivingEndpoint("http://api.linkedin.com/v1/people/~", HttpDeliveryMethods.GetRequest);
var request = google.PrepareAuthorizedRequest(endpoint, accessToken);
var response = request.GetResponse();
return (new StreamReader(response.GetResponseStream())).ReadToEnd();
}
No, you don't need to use OpenID if you just want to access the user's Gmail. OpenID is for when you want to authenticate the user. OAuth is for when you want to access the user's data.
You need to include the scope parameter in your authorization request as described in this question: Adding scopes to OAuth 1.0 authorization request with DotNetOpenAuth.