I've got an ASP.NET Core application.
The configuration regarding Openiddict is as follows:
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore().UseDbContext<IdentityDataContext>();
options.Services.TryAddTransient<OpenIddictQuartzJob>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<QuartzOptions>, OpenIddictQuartzConfiguration>());
})
// Register the OpenIddict server components.
.AddServer(options =>
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
// Mark the "email", "profile" and "roles" scopes as supported scopes.
.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles)
// Note: the sample uses the code and refresh token flows but you can enable
// the other flows if you need to support implicit, password or client credentials.
.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
// Register the signing and encryption credentials.
.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate()
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableStatusCodePagesIntegration())
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
builder.Services.ConfigureApplicationCookie(options => options.LoginPath = "/account/auth");
In tests I use a server factory:
public class InMemoryWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<IdentityDataContext>))!;
services.Remove(descriptor);
services.AddDbContext<IdentityDataContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
// without this I get a NPE
options.UseOpenIddict();
});
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<IdentityDataContext>();
db.Database.EnsureCreated();
});
protected override void ConfigureClient(HttpClient client)
{
base.ConfigureClient(client);
// without this I get Bad request due to Opeiddict filters
client.DefaultRequestHeaders.Host = client.BaseAddress!.Host;
}
}
The test looks like this (taken from here):
[Fact]
public async Task AuthorizedRequestReturnsValue()
{
var client = _factory.WithWebHostBuilder(builder => builder
.ConfigureTestServices(services => services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestScheme", _ => { })))
.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("TestScheme");
var response = await client.GetAsync(new Uri("https://localhost/connect/userinfo"));
// I get Unauthorized here instead
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
The /connect/userinfo is as follows:
[HttpGet("~/connect/userinfo")]
[HttpPost("~/connect/userinfo")]
[IgnoreAntiforgeryToken]
[Produces("application/json")]
public async Task<IActionResult> Userinfo()
{
var user = await _userManager.FindByIdAsync(User.GetClaim(Claims.Subject)!);
if (user is null)
{
return Challenge(
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The specified access token is bound to an account that no longer exists.",
}),
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
// Note: the "sub" claim is a mandatory claim and must be included in the JSON response.
[Claims.Subject] = await _userManager.GetUserIdAsync(user),
};
claims[Claims.Email] = (await _userManager.GetEmailAsync(user))!;
claims[Claims.EmailVerified] = await _userManager.IsEmailConfirmedAsync(user);
claims[Claims.Name] = (await _userManager.GetUserNameAsync(user))!;
claims[Claims.PhoneNumber] = (await _userManager.GetPhoneNumberAsync(user))!;
claims[Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user);
claims[Claims.Role] = await _userManager.GetRolesAsync(user);
// Note: the complete list of standard claims supported by the OpenID Connect specification
// can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
return Ok(claims);
}
As far as I understand by default the TestScheme should be used for authentication. But it is not and OpenidDict takes precedence.
Is there a way to make authenticated/authorized requests in integration tests?
P.S. The test was working OK until I added OpenIddict. Before that I used Asp Identity directly for authentication
Related
I'm converting a Webforms App from using on premises LDAP Auth (System.DirectoryServices.AccountManagement , System.Security) to Azure AD , with SSO and AAD roles.
SSO is working, but when I try access one of my Rest API's , I get an HTTP response code 302 (Found /redirected) to/from https://login.microsoftonline.com - obviously because of SSO.
the API's are written in the code behind file (aspx.cs) in this fashion
[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public static List<someclass> GetThings()
{
List<someclass> dict = new BusinessObjects().GetStuff();
return dict;
}
What is wrong here?
Do I need to add Authentication to the API's? should it be token/bearer ?
my StartupAuth.cs looks like this :
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = (context) =>
{
return System.Threading.Tasks.Task.FromResult(0);
},
//custom code
SecurityTokenValidated = (context) =>
{
var claims = context.AuthenticationTicket.Identity.Claims;
//add Azure Active Directory Groups to the context. Requires "groupMembershipClaims": "SecurityGroup", in manifest file in Azure Portal
var groups = from c in claims
where c.Type == "groups"
select c;
foreach (var group in groups)
{
context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, group.Value));
}
//add Azure Active Directory Roles to the context. Requires entries in "appRoles": [] in manifest file in Azure Portal
var roles = from r in claims
where r.Type == "roles"
select r;
foreach (var role in roles)
{
context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role.Value));
}
return Task.FromResult(0);
},
//AuthorizationCodeReceived = (context) => {
//}
}
}
);
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
}
Old code:
Client = new HttpClient(new HttpClientHandler() { DefaultProxyCredentials = CredentialCache.DefaultNetworkCredentials });
// set an default user agent string, some services does not allow emtpy user agents
if (!Client.DefaultRequestHeaders.Contains("User-Agent"))
Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
Trying to implement the same using the new ASP.NET Core 2.1 HttpClientFactory:
services.AddHttpClient("Default", client =>
{
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0");
}).ConfigurePrimaryHttpMessageHandler(handler => new HttpClientHandler() { DefaultProxyCredentials = CredentialCache.DefaultNetworkCredentials });
Unfortunately, I get an HTTP 407 (Proxy Auth) error.
What I'm doing wrong?
It is usually advised to have a static class containing string constants for the names of the clients.
Something like:
public static class NamedHttpClients {
public const string Default = "Default";
}
Then ensure that named client is configured correctly, which in your particular case would look like:
services
.AddHttpClient(NamedHttpClients.Default, client => {
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {
DefaultProxyCredentials = CredentialCache.DefaultNetworkCredentials
});
From there you can get the client from an injected IHttpClientFactory
var client = httpClientFactory.CreateClient(NamedHttpClients.Default);
and used as intended.
I'm currently trying to automatically setup a graphservice whenever my application starts. I have following code:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
})
.AddCookie();
services.AddMvc();
}
Inside or after the AddAzureAd I'd like to register and configure a GraphService to connect to MS AAD Graph Api https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api
Yet I have no idea how to get an accesstoken which every example speaks of. I ticked the box on the template "Read" from Graph API, so I though this would be configured automatically, sadly it isn't.
To acquire the access token in the asp.net core with OpenIdConnect protocol, we need to use OnAuthorizationCodeReceived event like code below:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = ClientId,
Authority = Authority,
PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
GetClaimsFromUserInfoEndpoint = false,
Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
});
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Acquire a Token for the Graph API and cache it using ADAL. In the TodoListController, we'll use the cache to acquire a token to the Todo List API
string userObjectId = (context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code, new Uri(context.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]), clientCred, GraphResourceId);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption();
}
More detail about acquire access_token in the asp.net core, you can refer the code sample below:
active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore
I'd like to add refresh token functionnality in my API, so when a user in y application call the API, if its token is still valid, it will be refresh.
I added the authorizationController, here is the code :
[HttpPost("~/connect/token"), Produces("application/json")]
public IActionResult Exchange(OpenIdConnectRequest request)
{
if (request.IsPasswordGrantType())
{
// Validate the user credentials.
// Note: to mitigate brute force attacks, you SHOULD strongly consider
// applying a key derivation function like PBKDF2 to slow down
// the password validation process. You SHOULD also consider
// using a time-constant comparer to prevent timing attacks.
User user = new Models.User();
using (var db = new UserContext())
{
user = db.User.Where(u => u.login == request.Username).FirstOrDefault();
if (request.Username != user.login || request.Password != user.pwd)
{
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
}
// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIdConnectServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
// Add a "sub" claim containing the user identifier, and attach
// the "access_token" destination to allow OpenIddict to store it
// in the access token, so it can be retrieved from your controllers.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject,
"71346D62-9BA5-4B6D-9ECA-755574D628D8",
OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, user.login,
OpenIdConnectConstants.Destinations.AccessToken);
// ... add other claims, if necessary.
var principal = new ClaimsPrincipal(identity);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(principal, OpenIdConnectServerDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
and my Startup.cs code :
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<UserContext>(options =>
{
options.UseOpenIddict();
});
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddOpenIddict(options =>
{
options.AddEntityFrameworkCoreStores<UserContext>();
options.AddMvcBinders();
options.EnableTokenEndpoint("/connect/token");
options.AllowPasswordFlow().AllowRefreshTokenFlow()
.SetAccessTokenLifetime(TimeSpan.FromMinutes(20))
.SetRefreshTokenLifetime(TimeSpan.FromMinutes(10));
options.DisableHttpsRequirement();
});
services.AddDistributedMemoryCache();
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddFile("Logs/mylog-{Date}.txt");
loggerFactory.AddDebug();
app.UseOpenIddict();
app.UseOAuthValidation();
app.UseMvcWithDefaultRoute();
app.UseMvc();
}
The authentication and token generation are working, but not the refreshing of the token. I added SetAccessTokenLifetime(TimeSpan.FromMinutes(20)) and SetRefreshTokenLifetime(TimeSpan.FromMinutes(10));
The first one is working, my token expiration equals 20 minutes, but the token is never refreshed.
I have two Web APIs with a shared machine.key. I would like to pass the bearer token generated by the first Web API to the second Web API as a parameter (i.e. token=xxxxxxxx) and extract the identity claims (i.e userId) from it.
Is this possible? I've looked all over but there doesn't seem to be much information on parsing a text bearer token to extract claims.
Thanks.
If you're using OWIN, you could implement your own OAuthBearerAuthenticationProvider, which takes the token from the query string and sets it to the context:
internal class MyAuthProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
if (context.Token == null)
{
var value = context.Request.Query.Get("token");
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
}
return Task.FromResult<object>(null);
}
}
You could use it in your Startup.cs like this:
public void Configuration(IAppBuilder app)
{
// All the other stuff here
var audience = "";
var secret = "...";
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
Provider = new MyAuthProvider(),
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new [] { audience },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider("MyApp", TextEncodings.Base64Url.Decode(key))
}
});
// All the other stuff here
}
When you've implemented your auth like this, you can access the token information in your WebApi controller via the User.Identity property. To read custom claims, you can cast it to ClaimsIdentity.
var identity = User.Identity as ClaimsIdentity;
var myClaim = identity.Claims.FirstOrDefault(c => c.Type == "myClaimKey");