I am using MS Tests to write Unit tests for controller that authorizes the user using OAuth. I understand it is not a great idea to Moq HttpContext. Can I get help with setting up GetOpenIddictServerRequest().
The Controller End point is
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// If prompt=login was specified by the client application,
// immediately return the user agent to the login page.
if (request.HasPrompt(Prompts.Login))
{
// To avoid endless login -> authorization redirects, the prompt=login flag
// is removed from the authorization request payload before redirecting the user.
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
var parameters = Request.HasFormContentType ?
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
```
The code Snipped looks like
public static OpenIddictRequest? GetOpenIddictServerRequest(this HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
return context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction?.Request;
}
I tried setting up HttpContext in my TestMethod as:
_authorizationController.ControllerContext = new ControllerContext() { HttpContext = new DefaultHttpContext() { } };
Related
I have a .NET MVC Core 3.1 Webapp running on azure. This webapp is with SSO against Azure AD and is consumming powerbi API and graph API in delegated mode.
All was working fine but now I regularly have failed_to_acquire_token_silently Exceptions when AcquireTokenSilentAsync is triggered. This is not 100% of the times and happears to me a bit randomly.
Let me try to extract what I think are the most relevant code parts.
Startup.cs / ConfigureServices:
services.AddAuthentication("Azures").AddPolicyScheme("Azures", "Authorize AzureAd or AzureAdBearer", options =>
{
options.ForwardDefaultSelector = context =>
{
....
};
})
.AddJwtBearer(x =>
{
.....
})
// For browser access
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
Startup.cs / ConfigureTokenHandling:
private void ConfigureTokenHandling(IServiceCollection services)
{
if (Configuration["AuthWithAppSecret:ClientSecret"] != "")
{
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ResponseType = Configuration["AuthWithAppSecret:ResponseType"];
options.ClientSecret = Configuration["AuthWithAppSecret:ClientSecret"];
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
HttpRequest request = ctx.HttpContext.Request;
//We need to also specify the redirect URL used
string currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
//Credentials for app itself
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
//Construct token cache
ITokenCacheFactory cacheFactory = ctx.HttpContext.RequestServices.GetRequiredService<ITokenCacheFactory>();
TokenCache cache = cacheFactory.CreateForUser(ctx.Principal);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
string resource = Configuration["PowerBI:PowerBiResourceUrl"];
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, resource);
//Tell the OIDC middleware we got the tokens, it doesn't need to do anything
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
}
}
A controller is like this :
public class ProjectsController : BaseController
{
private readonly ITokenCacheFactory _tokenCacheFactory;
public ProjectsController(MyContext context, IConfiguration configuration, ITokenCacheFactory tokenCacheFactory)
{
_context = context;
_tokenCacheFactory = tokenCacheFactory;
_configuration = configuration;
}
Later triggered by the controller:
static public async Task<string> GetAccessTokenAsync2(IConfiguration _configuration, ITokenCacheFactory _tokenCacheFactory, ClaimsPrincipal User, string resURL, Uri redirectURI)
{
string authority = _configuration["AzureAd:Authority"];
string clientId = _configuration["AzureAd:ClientId"];
string clientSecret = _configuration["AuthWithAppSecret:ClientSecret"];
var cache = _tokenCacheFactory.CreateForUser(User);
var authContext = new AuthenticationContext(authority, cache);
var credential = new ClientCredential(clientId, clientSecret);
var userId = User.GetObjectId();
AuthenticationResult result;
try
{
result = await authContext.AcquireTokenSilentAsync(
resURL,
credential,
new UserIdentifier(userId, UserIdentifierType.UniqueId));
}
catch (AdalException ex)
{
mylog.Info("GetAccessTokenAsync - Adal Ex:" + ex.ErrorCode);
if (ex.ErrorCode == "failed_to_acquire_token_silently")
{
// There are no tokens in the cache.
try
{
PlatformParameters param = new PlatformParameters();
result = await authContext.AcquireTokenAsync(resURL, clientId, redirectURI, param, new UserIdentifier(userId, UserIdentifierType.UniqueId));
}
catch (Exception e)
{
mylog.Error("GetAccessTokenAsync - AcquireTokenAsync" + e.ToString());
throw e;
}
}
else
throw ex;
}
return result.AccessToken;
}
AcquireTokenAsync has been added to turn around the failed_to_acquire_token_silently issue (but it is totaly failling).
Do you have any idea why it is failing from time to time ?
Any other idea how to fix it ?
Thanks!!!
Christian
EDIT 07/04:
Here an example:
2021-04-07 15:18:24.674 +00:00 OnAuthorizationCodeReceived is triggered for user fd918ddf-fbb9-40d2-812b-b01876118f42
2021-04-07 15:18:31.675 +00:00 AcquireTokenSilentAsync - trigger exception userId 'fd918ddf-fbb9-40d2-812b-b01876118f42'
The users is authenticated against AD correctly. A code is received and few seconds later there a failed_to_acquire_token_silently exception raised.
The error failed_to_acquire_token_silently occurs when an access token cannot be found in the cache or the access token is expired.
Code sample here:
// STS
string cloud = "https://login.microsoftonline.com";
string tenantId = "331e6716-26e8-4651-b323-2563936b416e";
string authority = $"{cloud}/{tenantId}";
// Application
string clientId = "65b27a1c-693c-44bf-bf92-c49e408ccc70";
Uri redirectUri = new Uri("https://TodoListClient");
// Application ID of the Resource (could also be the Resource URI)
string resource = "eab51d24-076e-44ee-bcf0-c2dce7577a6a";
AuthenticationContext ac = new AuthenticationContext(authority);
AuthenticationResult result=null;
try
{
result = await ac.AcquireTokenSilentAsync(resource, clientId);
}
catch (AdalException adalException)
{
if (adalException.ErrorCode == AdalError.FailedToAcquireTokenSilently
|| adalException.ErrorCode == AdalError.InteractionRequired)
{
result = await ac.AcquireTokenAsync(resource, clientId, redirectUri,
new PlatformParameters(PromptBehavior.Auto));
}
}
Note that, AcquireTokenSilent does not need to be called in the Client credentials flow (when the application acquires token without a
user, but in its own name)
But you use client credentials flow in your code, you could get access token via AcquireTokenAsync.
clientCredential = new ClientCredential(clientId, appKey);
AuthenticationContext authenticationContext =
new AuthenticationContext("https://login.microsoftonline.com/<tenantId>");
AuthenticationResult result =
await authenticationContext.AcquireTokenAsync("https://resourceUrl",
clientCredential);
I have implemented an OpenIdConnect with Azure. The code is approximately like this:
var options = new OpenIdConnectOptions
{
SignInScheme = PersistentSchemeName,
CallbackPath = "/oauth2office2",
ClientId = pubConf.ApplicationId,
Authority = $"https://login.microsoftonline.com/{configuration.TenantId}"
};
It works perfectly.
But I also need admin consent and I don't want my users to add two CallbackPaths into my app.
So I crafted admin consent url manually.
And added a redirect so it won't conflict with a OpenId middleware:
app.UseRewriter(new RewriteOptions().Add(context =>
{
var request = context.HttpContext.Request;
if (request.Path.StartsWithSegments("/oauth2office2") && request.Method == HttpMethods.Get)
{
request.Path = "/oauth2office";
}
}));
Now i have a controller at /oauth2office that does some extra stuff for me (actually gets tenant id).
Question - is there a way I can achieve it with OpenIdConnect middleware? While still being on the same callback path.
Because adding two paths is an extra i want to avoid.
I'm not even sure I can make OpenIdConnect work with admin consent actually.
One option is to add two AddOpenIDConnect(...) instances with different schema names and different callback endpoints?
You can only have one endpoint per authentication handler.
Also, do be aware that the callback request to the openidconnect handler is done using HTTP POST, like
POST /signin-oidc HTTP/1.1
In your code you are looking for a GET
if (request.Path.StartsWithSegments("/oauth2office2") && request.Method == HttpMethods.Get)
This can be done with a single OpenIdConnect handler by overriding the events RedirectToIdentityProvider and MessageReceived.
public override async Task RedirectToIdentityProvider(RedirectContext context)
{
if (!context.Properties.Items.TryGetValue("AzureTenantId", out var azureTenantId))
azureTenantId = "organizations";
if (context.Properties.Items.TryGetValue("AdminConsent", out var adminConsent) && adminConsent == "true")
{
if (context.Properties.Items.TryGetValue("AdminConsentScope", out var scope))
context.ProtocolMessage.Scope = scope;
context.ProtocolMessage.IssuerAddress =
$"https://login.microsoftonline.com/{azureTenantId}/v2.0/adminconsent";
}
await base.RedirectToIdentityProvider(context);
}
public override async Task MessageReceived(MessageReceivedContext context)
{
// Handle admin consent endpoint response.
if (context.Properties.Items.TryGetValue("AdminConsent", out var adminConsent) && adminConsent == "true")
{
if (!context.ProtocolMessage.Parameters.ContainsKey("admin_consent"))
throw new InvalidOperationException("Expected admin_consent parameter");
var redirectUri = context.Properties.RedirectUri;
var parameters = context.ProtocolMessage.Parameters.ToQueryString();
redirectUri += redirectUri.IndexOf('?') == -1
? "?" + parameters
: "&" + parameters;
context.Response.Redirect(redirectUri);
context.HandleResponse();
return;
}
await base.MessageReceived(context);
}
Then when you need to do admin consent, craft a challenge with the correct properties:
public IActionResult Register()
{
var redirectUrl = Url.Action("RegisterResponse");
var properties = new OpenIdConnectChallengeProperties
{
RedirectUri = redirectUrl,
Items =
{
{ "AdminConsent", "true" },
{ "AdminConsentScope", "https://graph.microsoft.com/.default" }
}
};
return Challenge(properties, "AzureAd");
}
public IActionResult RegisterResponse(
bool admin_consent,
string tenant,
string scope)
{
_logger.LogInformation("Admin Consent for tenant {tenant}: {admin_consent} {scope}", tenant, admin_consent,
scope);
return Ok();
}
My app requirements is to authenticate using client credentials AND another code (hash).
I followed this link to create and use custom IExtensionGrantValidator.
I manged to invoke the custom IExtensionGrantValidator with approved grant, but client always gets invalid_grant error.
For some reason the set operation ofd Result (property of ExtensionGrantValidationContext) always fails (overriding the Error value returns the overrided value to client).
This is CustomGrantValidator Code:
public class CustomGrantValidator : IExtensionGrantValidator
{
public string GrantType => "grant-name";
public Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result
}
}
Startup.cs contains this line:
services.AddTransient<IExtensionGrantValidator, CustomGrantValidator>();
And finally client's code:
var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5000") };
var disco = await httpClient.GetDiscoveryDocumentAsync("http://localhost:5000");
var cReq = await httpClient.RequestTokenAsync(new TokenRequest
{
GrantType = "grant-name",
Address = disco.TokenEndpoint,
ClientId = clientId,// client Id taken from appsetting.json
ClientSecret = clientSecret, //client secret taken from appsetting.json
Parameters = new Dictionary<string, string> { { "hash", hash } }
});
if (cReq.IsError)
//always getting 'invalid_grant' error
throw InvalidOperationException($"{cReq.Error}: {cReq.ErrorDescription}");
The below codes works on my environment :
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result;
return;
}
Don't forget to register the client to allow the custom grant :
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = { "grant-name" },
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
I got the same issue and found the answer from #Sarah Lissachell, turn out that I need to implement the IProfileService. This interface has a method called IsActiveAsync. If you don't implement this method, the answer of ValidateAsync will always be false.
public class IdentityProfileService : IProfileService
{
//This method comes second
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//IsActiveAsync turns out to be true
//Here you add the claims that you want in the access token
var claims = new List<Claim>();
claims.Add(new Claim("ThisIsNotAGoodClaim", "MyCrapClaim"));
context.IssuedClaims = claims;
}
//This method comes first
public async Task IsActiveAsync(IsActiveContext context)
{
bool isActive = false;
/*
Implement some code to determine that the user is actually active
and set isActive to true
*/
context.IsActive = isActive;
}
}
Then you have to add this implementation in your startup page.
public void ConfigureServices(IServiceCollection services)
{
// Some other code
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<Users>()
.AddInMemoryApiResources(config.GetApiResources())
.AddExtensionGrantValidator<CustomGrantValidator>()
.AddProfileService<IdentityProfileService>();
// More code
}
I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}
In following this tutorial (modifying it to use an application-based auth string rather than their user model), have the following TokenValidationAttribute defined and set this attribute on WebAPI controllers in order to verify that the API request came within my web application:
public class TokenValidationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
string token;
try
{
token = actionContext.Request.Headers.GetValues("Authorization-Token").First();
}
catch (Exception)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent("Missing Authorization-Token")
};
return;
}
try
{
var crypto = new SimpleCrypto.PBKDF2(); // type of encryption
var authPart = ConfigurationManager.AppSettings["AuthorizationTokenPart"];
var authSalt = GlobalVariables.AuthorizationSalt;
var authToken = GlobalVariables.AuthorizationToken;
if (authToken == crypto.Compute(authPart, authSalt))
{
// valid auth token
}
else
{
// invalid auth token
}
//AuthorizedUserRepository.GetUsers().First(x => x.Name == RSAClass.Decrypt(token));
base.OnActionExecuting(actionContext);
}
catch (Exception ex)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
Content = new StringContent("Unauthorized User")
};
return;
}
}
}
In my login class, I have the following method defined that returns a User object if valid:
private User IsValid(string username, string password)
{
var crypto = new SimpleCrypto.PBKDF2(); // type of encryption
using (var db = new DAL.DbContext())
{
var user = db.Users
.Include("MembershipType")
.FirstOrDefault(u => u.UserName == username);
if (user != null && user.Password == crypto.Compute(password, user.PasswordSalt))
{
return user;
}
}
return null;
}
As you can see, the user login validation method doesn't make a WebAPI call that would be to ~/api/User (that part works).
1) How do I generate a request with with auth token (only site-generated API requests are valid)? These could be direct API calls from code-behind, or JavaScript-based (AngularJS) requests to hydrate some objects.
2) I'm not entirely clear on what base.OnActionExecuting(actionContext); . What do I do if the token is valid/invalid?
i think the best practices to send authorization header is by added it on request header
request.Headers.Add("Authorization-Token",bla bla bla);
you can create webrequest or httprequest
maybe you should start from http://rest.elkstein.org/2008/02/using-rest-in-c-sharp.html
or http://msdn.microsoft.com/en-us/library/debx8sh9%28v=vs.110%29.aspx.
in my opinion in order to create proper login security and request you should apply a standard such as openid or oauth
cheers
I did something like this, LoginSession contains my token and is static (in my case its a shared service (not static))
public HttpClient GetClient()
{
var client = new HttpClient
{
Timeout = new TimeSpan(0, 0, 2, 0),
BaseAddress = new Uri(GetServiceAddress())
};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (LoginSession.Token != null)
{
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", String.Format("Bearer {0}", LoginSession.Token.AccessToken));
}
return client;
}
notice this line:
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", String.Format("Bearer {0}", LoginSession.Token.AccessToken));