I'm trying a simple implementation in my Asp net Core application of Saml2 to integrate with an Ad FS server. I can't figure why I am getting this error. I downloaded the samples from the gitHub and tried to adapt it in my application.
NotImplementedException: The method or operation is not implemented.
Sustainsys.Saml2.AspNetCore2.Saml2Handler.AuthenticateAsync()
Here's my implementation, my application is running on Asp Net Core
On StartUp
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = Saml2Defaults.Scheme;
})
.AddSaml2(options =>
{
options.SPOptions.EntityId = new EntityId("http://myAdfsServer.myDomain.com/adfs/services/trust");
options.SPOptions.ReturnUrl = new Uri("https://localhost:5000");
options.IdentityProviders.Add(
new IdentityProvider(new EntityId("http://myAdfsServer.myDomain.com/adfs/services/trust"), options.SPOptions)
{
LoadMetadata = true,
MetadataLocation = "https://myAdfsServer.myDomain.com/FederationMetadata/2007-06/FederationMetadata.xml"
//MetadataLocation = "FederationMetadata.xml"
});
//options.SPOptions.ServiceCertificates.Add(new X509Certificate2(certificate.ToString()));
})
.AddCookie();
On my Controller
trying something similar to Sustainsys SAML2 Sample for ASP.NET Core WebAPI without Identity
[Authorize(AuthenticationSchemes = Saml2Defaults.Scheme)]
public class AuthenticationController : Controller
{
public AuthenticationController()
{
}
[AllowAnonymous]
public async Task LoginAdfs()
{
string redirectUri = string.Concat("https://localhost:5000", "/verifyAdfs");
try
{
new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(LoginCallback), new { redirectUri })
});
}catch(Exception e)
{
}
}
[AllowAnonymous]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(Saml2Defaults.Scheme);
//_log.Information("Authenticate result: {#authenticateResult}", authenticateResult);
// I get false here and no information on claims etc.
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
var claimsIdentity = new ClaimsIdentity("Email");
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
// _log.Information("Logged in user with following claims: {#Claims}", authenticateResult.Principal.Claims);
await HttpContext.SignInAsync("Email", new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
}
note: I've got a client that won't expose his MetaData in a URL, so I'll need to adapt it and set manually the metadata parameters
I'm stuck in this error, I does not even hit my method LoginAdfs.
The Saml2 handler cannot be used as an authencation scheme, it is a challenge scheme.
I guess that the LoginAdfs() method works fine, but that it's the LoginCallback that fails. The reason should be the call to HttpContext.AuthenticationAsync(Saml2Defaults.Scheme).
You should instead authenticate with the cookie scheme - because that's what keeps the session. Internally when the challenge is completed, the Saml2 handler will use the DefaultSignInScheme to preserve the result in a session (through a cookie, as that's the default sign in scheme).
Related
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() { } };
I have an application where the backend is an asp.net web api and the front-end is a Blazor server side. Both projects are using net6.0.
I have implemented jwt token authentication, so users can register and login from the front-end.
My problem is that if the user refreshes a page, he automatically gets logged out. My understanding is that this can be solved using refresh token (I'm not sure if this understanding is correct).
I have tried to follow this guide: Refresh Token with Blazor WebAssembly and ASP.NET Core Web API
However since I'm using Blazor server side I cannot intercept HTTP Requests using the approach in the article.
My question is: in my Blazor server side application how can I prevent users automatically getting logged out due to page refresh and how can I intercept the http request?
UPDATE: Notice I already have everything working in regards to token and authentication between the back and frontend. The part that I'm missing is inside the blazor server side application in the program.cs file. I basically want to intercept all http request and call a method.
In program.cs I have:
builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
I want RefreshTokenService to be called on every http request. I have tried creating a middleware (which calls the RefreshTokenService), inside the program.cs like:
app.UseMyMiddleware();
But this only get called once.
Here's a very simplified version of an API client I'm using in my app that's also split into an ASP.NET Core API backend and a Blazor Server frontend.
The way it works is that the accessToken gets retreived from local storage and added as an authentication header to the HttpRequestMessage in my API client before each API call.
MyApiClient.cs
public class MyApiClient
{
private readonly IHttpClientFactory _clientFactory;
private readonly IMyApiTokenProvider _myApiTokenProvider;
public MyApiClient(IHttpClientFactory clientFactory, IMyApiTokenProvider myApiTokenProvider)
{
_clientFactory = clientFactory;
_myApiTokenProvider = myApiTokenProvider;
}
public async Task<ApiResponse<CustomerListResponse>> GetCustomersAsync()
{
//create HttpClient
var client = _clientFactory.CreateClient("MyApiHttpClient");
//create HttpRequest
var request = CreateRequest(HttpMethod.Get, "/getCustomers");
//call the API
var response = await client.SendAsync(request);
//if Unauthorized, refresh access token and retry
if(response.StatusCode == HttpStatusCode.Unauthorized)
{
var refreshResult = await RefreshAccessToken(client);
if (refreshResult.IsSuccess)
{
//save new token
await _backendTokenProvider.SetAccessToken(refreshResult.NewAccessToken);
//create request again, with new access token
var retryRequest = await CreateRequest(HttpMethod.Get, "/getCustomers");
//retry
response = await client.SendAsync(retryRequest);
}
else
{
//refresh token request failed
return ApiResponse<CustomerListResponse>.Error("Token invalid");
}
}
//parse response
var customers = await response.Content.ReadFromJsonAsync<ApiResponse<CustomerListResponse>>();
return customers;
}
private HttpRequestMessage CreateRequest<TRequest>(string command, HttpMethod method, TRequest requestModel = null) where TRequest : class
{
//create HttpRequest
var request = new HttpRequestMessage(method, command);
//add body if not empty
if (requestModel is not null)
{
request.Content = JsonContent.Create(requestModel);
}
//set the Auth header to the Access Token value taken from Local Storage
var accessToken = await _myApiTokenProvider.GetAccessToken();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return request;
}
private async Task<ApiResponse<RefreshTokenResponse>> RefreshAccessToken(HttpClient client)
{
var refreshToken = await _backendTokenProvider.GetRefreshToken();
if (refreshToken is null)
{
return ApiResponse<RefreshTokenResponse>.Error("Refresh token is null, cannot refresh access token");
}
var refreshRequest = CreateRequest(HttpMethod.Post, "/refreshToken", new RefreshTokenRequest(refreshToken));
var refreshResponse = await client.SendAsync(refreshRequest);
var refreshResult = await response.Content.ReadFromJsonAsync<ApiResponse<RefreshTokenResponse>>();
return refreshResult;
}
}
MyApiTokenProvider.cs
public class MyApiTokenProvider : IMyApiTokenProvider
{
private readonly ProtectedLocalStorage _protectedLocalStorage;
public MyApiTokenProvider(ProtectedLocalStorage protectedLocalStorage)
{
_protectedLocalStorage = protectedLocalStorage;
}
public async Task<string> GetAccessToken()
{
var result = await _protectedLocalStorage.GetAsync<string>("accessToken");
return result.Success ? result.Value : null;
}
public async Task<string> GetRefreshToken()
{
var result = await _protectedLocalStorage.GetAsync<string>("refreshToken");
return result.Success ? result.Value : null;
}
public async Task SetAccessToken(string newAccessToken)
{
await _protectedLocalStorage.SetAsync("accessToken", newAccessToken);
}
public async Task SetRefreshToken(string newRefreshToken)
{
await _protectedLocalStorage.SetAsync("refreshToken", newRefreshToken);
}
}
We are using Sustainsys middleware with .NET Core to connect to an SAML2 IDP. It works well.
However, when we add more than one IDP in the Startup.cs we get in trouble. The user will select which IDP to login to and then the code should send a challenge to that IDP.
How to we specify which IDP in the code?
Using standard .NET Framework it is straight forward:
Context.GetOwinContext().Environment.Add("saml2.idp", new Entity(IDP2EntityId));
but there is no such construct in the .NET Core middleware.
Here is my code. Basically I add two IDPs during startup but I don't know how to specify which one during login/challenge? With this code IDP-1 is always selected because it was the first one added.
STARTUP.CS
public void ConfigureServices(IServiceCollection services)
{
var authenticationBuilder = GetAuthenticationBuilder(services);
string authenticationScheme = "saml2.idp"
authenticationBuilder.AddSaml2(authenticationScheme, options =>
{
options.SPOptions = GetSPOptions();
// Add IDP-1
options.IdentityProviders.Add(
new IdentityProvider(new EntityId(IDPEntityUrl1), options.SPOptions)
{
MetadataLocation = IDPMetadataUrl1
});
// Add IDP-2
options.IdentityProviders.Add(
new IdentityProvider(new EntityId(IDPEntityUrl2), options.SPOptions)
{
MetadataLocation = IDPMetadataUrl2
});
}
}
LOGINCONTROLLER.CS
string saml2AuthenticationScheme = "saml2.idp";
var props = new AuthenticationProperties
{
RedirectUri = returnUrl,
Items = { { "scheme", saml2AuthenticationScheme } }
};
return Challenge(properties: props, saml2AuthenticationScheme);
How do I specify which IDP to use in the LoginController?
I found the solution. We studied the Sustainsys code and found the undocumented (?) feature to specify the IDP in the AuthenticationProperties.Items with an "idp" item. Like this:
LoginController.cs
string saml2AuthenticationScheme = "saml2.idp";
var props = new AuthenticationProperties
{
RedirectUri = returnUrl,
Items = { { "scheme", saml2AuthenticationScheme }, { "idp", theSelectedIDPIdentityId } }
};
return Challenge(properties: props, saml2AuthenticationScheme);
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();
}
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;
}
}