I'm trying to do TDD while developing but I am struggling. My first test I can successfully get the options set in services.AddAuthentication(...), but I am not able to get the options from .AddCookie(..) that is added onto the previous call. During debug I do see that there is an IPostConfigureOptions added for CookieAuthenticationOptions, and I suspect that it somehow alters the default option values, but I don't know how to get it.
Code to be tested:
public static void AddOpenIdConnect(this IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options => { options.MinimumSameSitePolicy = SameSiteMode.None; });
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(
CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.ExpireTimeSpan = TimeSpan.FromHours(2);
options.SlidingExpiration = true;
options.AccessDeniedPath = new PathString("/Error/403");
}
);
}
My first test ensured the AuthenticationOptions were set, and it passes:
[TestMethod]
public void AddOpenIdConnect_Should_AddAuthenticationOptions()
{
// arrange
var services = new ServiceCollection();
// act
services.AddOpenIdConnect();
// assert
var provider = services.BuildServiceProvider();
var authOptions = provider.GetRequiredService<IOptions<AuthenticationOptions>>();
authOptions.Value.DefaultScheme.Should().Be(CookieAuthenticationDefaults.AuthenticationScheme);
authOptions.Value.DefaultChallengeScheme.Should().Be(OpenIdConnectDefaults.AuthenticationScheme);
}
This method fails, as AccessDeniedPath returns the default setting and not the setting that the code being tested sets (you can see that I commented out the check on ExpireTimeSpan as it fails as well.)
[TestMethod]
public void AddOpenIdConnect_Should_AddCookieAuthenticationOptions()
{
// arrange
var services = new ServiceCollection();
// act
services.AddOpenIdConnect();
// assert
var provider = services.BuildServiceProvider();
var authCookieOptions = provider.GetRequiredService<IOptions<CookieAuthenticationOptions>>();
//authCookieOptions.Value.ExpireTimeSpan.Should().Be(TimeSpan.FromHours(2));
authCookieOptions.Value.SlidingExpiration.Should().BeTrue();
authCookieOptions.Value.AccessDeniedPath.Should().Be("/Error/403");
}
Any help would be much appreciated, as I still have to tack on a ".AddOpenIdConnect(options =>" after this.
Thank you!
The source code for the AddCookie extension shows that is uses the authentication scheme to configure a named option when adding CookieAuthenticationOptions
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder,
string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme).Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
}
Source Code
Note the
builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme) //<-- Named option
Thus the same scheme used when the option was registered will need to be used in order to access the name option
This can be done using IOptionsSnapshot<TOptions>.Get(String) Method
//...omitted for brevity
// Assert
var provider = services.BuildServiceProvider();
IOptionsSnapshot<CookieAuthenticationOptions> namedOptionsAccessor =
provider.GetRequiredService<IOptionsSnapshot<CookieAuthenticationOptions>>();
CookieAuthenticationOptions options =
namedOptionsAccessor.Get(CookieAuthenticationDefaults.AuthenticationScheme);
options.ExpireTimeSpan.Should().Be(TimeSpan.FromHours(2));
options.SlidingExpiration.Should().BeTrue();
options.AccessDeniedPath.Should().Be("/Error/403");
Related
I have created two project and setup server in one project and in another setup a client. I have created database table to store the client information. I have also stored the RedirectUris in the database but every time I am getting the above error.
Can any one please suggest me what I am missing?
Server code:
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore().UseDbContext<AuthDbContext>();
// Enable Quartz.NET integration.
options.UseQuartz();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, logout, token and userinfo endpoints.
options
.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo");
options
.AllowClientCredentialsFlow()
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
options
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption();
options.AllowPasswordFlow();
// Add all auth flows that you want to support
// Register your scopes
// Scopes are a list of identifiers used to specify what access privileges are requested.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
// Set the lifetime of your tokens
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));
options.SetRefreshTokenLifetime(TimeSpan.FromDays(7));
// Register ASP.NET Core host and configure options
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.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();
});
Client code:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(options =>
{
// Note: these settings must match the application details
// inserted in the database at the server level.
options.ClientId = {ClientId};
options.ClientSecret = {ClientSecret };
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
// Use the authorization code flow.
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Authority = "https://localhost:44312";
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("profile");
options.ClaimActions.MapJsonKey(ClaimTypes.Uri, ClaimTypes.Uri, "string");
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, ClaimTypes.GivenName, "string");
options.ClaimActions.MapJsonKey(ClaimTypes.Surname, ClaimTypes.Surname, "string");
options.SecurityTokenValidator = new JwtSecurityTokenHandler
{
// Disable the built-in JWT claims mapping feature.
InboundClaimTypeMap = new Dictionary<string, string>()
};
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.Events.OnSignedOutCallbackRedirect += context =>
{
context.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
I have checked so many documents from the internet but didn't found any proper solutions
Create a client and set the redirectURI
public class TestData : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public TestData(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<FaDbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var client = await manager.FindByClientIdAsync("postman", cancellationToken);
if (client is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "postman",
ClientSecret = "postman-secret",
DisplayName = "Postman Client",
RedirectUris = { new Uri("https://oauth.pstmn.io/v1/callback") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
//OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.GrantTypes.Password,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Roles,
OpenIddictConstants.Permissions.Scopes.Address,
OpenIddictConstants.Permissions.Scopes.Phone,
OpenIddictConstants.Permissions.Prefixes.Scope + "faid_client_scope",
OpenIddictConstants.Permissions.Prefixes.Scope + "test_scope_1",
OpenIddictConstants.Permissions.ResponseTypes.Code,
OpenIddictConstants.Permissions.ResponseTypes.IdToken,
OpenIddictConstants.Permissions.ResponseTypes.CodeIdToken,
OpenIddictConstants.Permissions.ResponseTypes.Token
},
}, cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Then register in program.cs
builder.Services.AddHostedService<TestData>();
there is a guide on how to do it here:
https://dev.to/robinvanderknaap/setting-up-an-authorization-server-with-openiddict-part-iii-client-credentials-flow-55lp
My ConfigureService method in startup.cs class
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson();
services.AddSingleton<IConfiguration>(this.Configuration);
// Load settings
var settings = new BotSettings();
Configuration.Bind(settings);
services.AddDbContext<BotDbContext>(options => options.UseSqlServer(settings.ConnectionString));
// Create the credential provider to be used with the Bot Framework Adapter.
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
services.AddSingleton<BotAdapter>(sp => (BotFrameworkHttpAdapter)sp.GetService<IBotFrameworkHttpAdapter>());
// Register AuthConfiguration to enable custom claim validation for skills.
services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedCallersClaimsValidator(settings.SkillConfiguration) });
// register components.
ComponentRegistration.Add(new DialogsComponentRegistration());
ComponentRegistration.Add(new DeclarativeComponentRegistration());
ComponentRegistration.Add(new AdaptiveComponentRegistration());
ComponentRegistration.Add(new LanguageGenerationComponentRegistration());
ComponentRegistration.Add(new QnAMakerComponentRegistration());
ComponentRegistration.Add(new LuisComponentRegistration());
// register Handoff
ConfigureHandOff(services, settings);
// This is for custom action component registration.
ComponentRegistration.Add(new CustomActionComponentRegistration());
// Register the skills client and skills request handler.
services.AddSingleton<SkillConversationIdFactoryBase, SkillConversationIdFactory>();
services.AddHttpClient<BotFrameworkClient, SkillHttpClient>();
services.AddSingleton<ChannelServiceHandler, SkillHandler>();
services.AddApplicationInsightsTelemetry(settings?.ApplicationInsights?.InstrumentationKey ?? string.Empty);
services.AddSingleton<ITelemetryInitializer, OperationCorrelationTelemetryInitializer>();
services.AddSingleton<ITelemetryInitializer, TelemetryBotIdInitializer>();
services.AddSingleton<IBotTelemetryClient, BotTelemetryClient>();
services.AddSingleton<TelemetryLoggerMiddleware>(sp =>
{
var telemetryClient = sp.GetService<IBotTelemetryClient>();
return new TelemetryLoggerMiddleware(telemetryClient, logPersonalInformation: settings?.Telemetry?.LogPersonalInformation ?? false);
});
services.AddSingleton<TelemetryInitializerMiddleware>(sp =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var telemetryLoggerMiddleware = sp.GetService<TelemetryLoggerMiddleware>();
return new TelemetryInitializerMiddleware(httpContextAccessor, telemetryLoggerMiddleware, settings?.Telemetry?.LogActivities ?? false);
});
var storage = ConfigureStorage(settings);
services.AddSingleton(storage);
var userState = new UserState(storage);
var conversationState = new ConversationState(storage);
services.AddSingleton(userState);
services.AddSingleton(conversationState);
//Configure bot loading path
var botDir = settings.Bot;
var resourceExplorer = new ResourceExplorer().AddFolder(botDir);
var defaultLocale = Configuration.GetValue<string>("defaultLanguage") ?? "en-us";
var rootDialog = GetRootDialog(botDir);
services.AddSingleton(resourceExplorer);
resourceExplorer.RegisterType<OnQnAMatch>("Microsoft.OnQnAMatch");
services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>(s =>
GetBotAdapter(storage, settings, userState, conversationState, s));
var removeRecipientMention = settings?.Feature?.RemoveRecipientMention ?? false;
//Adding Required Services
services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient<IUserService, UserService>();
services.AddTransient<ICommunicationService, CommunicationService>();
services.AddTransient<IMessageService, MessageService>();
services.AddSingleton<IBot>(s =>
new ComposerBot(
s.GetService<IUserService>(),
s.GetService<ConversationState>(),
s.GetService<UserState>(),
s.GetService<MessageRouter>(),
s.GetService<MessageRouterResultHandler>()));
}
However when I am trying to access UserService Object it passing null object in ComposerBot.cs class? What could be the reason?
public ComposerBot(
IUserService userService,
ConversationState conversationState,
UserState userState,
MessageRouter messageRouter,
MessageRouterResultHandler messageRouterResultHandler)
{
this.userService = userService; **showing NULL**
this.conversationState = conversationState;
this.userState = userState;
this.dialogState = conversationState.CreateProperty<DialogState>("DialogState");
this.messageRouter = messageRouter;
this.messageRouterResultHandler = messageRouterResultHandler;
}
I think you are running into this issue:
https://github.com/dotnet/aspnetcore/issues/28684.
this is related:
https://github.com/dotnet/aspnetcore/issues/17442
A tempory solution at least for me was to inject the service in a razor page to get the user there and pass the user to the service.
note: this should be a comment but i dont have enough repuation to comment.
I want to implement OAuth in my web application and for that I added the following code in my startup.cs
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CombiTime API v1.0", Version = "v1" });
c.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("http://localhost:4200/login"),
TokenUrl = new Uri("http://localhost:4200/connect/token")
}
}
});
c.OperationFilter<AuthorizeOperationFilter>();
c.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "Bearer", //The name of the previously defined security scheme.
Type = ReferenceType.SecurityScheme
}
},new List<string>()
}
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned API v1.0");
c.DocumentTitle = "Title Documentation";
c.DocExpansion(DocExpansion.None);
c.RoutePrefix = string.Empty;
c.OAuthClientId("combitimeapi_swagger");
c.OAuthAppName("Combitime API");
c.OAuthUsePkce();
});
return app;
}
and the AuthorizeOperationFilter Code is as follows :
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Since all the operations in our api are protected, we need not
// check separately if the operation has Authorize attribute
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[
new OpenApiSecurityScheme
{
Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "oauth2"}
}
] = new[] {"combitimeapi"}
}
};
}
By using this code, I get an "Authorize" button on my swagger UI and when I click that button I am redirecting to my login page(front end based on angular). So I gave my AuthorizationUrl as http://localhost:4200/login and then when I am redirected to login page, I login with valid credentials, I have used jwt token for login and for that I added the following code in my startup.cs
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
I want to redirect back to the swagger UI after I login with valid credentials but the problem is that I am being redirected to the dashboard after I login. Please help me or let me know what I am doing wrong.
The url that is being formed after I am redirected to login page from swagger is :
http://localhost:4200/login?response_type=code&client_id=combitimeapi_swagger&redirect_uri=http:%2F%2Flocalhost:61574%2Foauth2-redirect.html&state=V2VkIEZlYiAxNyAyMDIxIDIyOjU3OjQ2IEdNVCswNTMwIChJbmRpYSBTdGFuZGFyZCBUaW1lKQ%3D%3D&code_challenge=mT0amBTJgczCZmNSZAYVfjzzpaTiGb68XlyR3RNHuas&code_challenge_method=S256
My front-end is running on port 4200.
My swagger is running on port 61574.
But I am not being redirected to swagger UI after putting in valid credentials
Please help me.
First, let me add some details to your picture:
You have two applications, one with API (based on ASP.NET Core) and one with frontend UI (Angular, but it doesn't matter), and, it's important, with authorization/authentication functions.
You use .NETCore 3.1
You configure an authorization for swagger that means any call from swagger UI page will use given authorization parameters.
So, for API application we have to add a class that has helper methods configuring our swagger:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CombiTime API v1.0", Version = "v1" });
c.AddSecurityDefinition(
"oauth2",
new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("https://lvh.me:4201/connect/authorize"),
TokenUrl = new Uri("https://lvh.me:4201/connect/token"),
Scopes = new Dictionary<string, string> {
{ "combitimeapi", "Demo API" }
}
}
}
});
c.OperationFilter<AuthorizeOperationFilter>();
c.AddSecurityRequirement(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "oauth2", //The name of the previously defined security scheme.
Type = ReferenceType.SecurityScheme
}
},
new List<string>()
}
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned API v1.0");
c.DocumentTitle = "Title Documentation";
c.DocExpansion(DocExpansion.None);
c.RoutePrefix = string.Empty;
c.OAuthClientId("combitimeapi_swagger");
c.OAuthAppName("Combitime API");
c.OAuthScopeSeparator(",");
c.OAuthUsePkce();
});
return app;
}
}
Please, pay attention to the AuthorizationUrl property and to the TokenUrl property. The AuthorizationUrl property should be pointed to our OAuth2 server authorization endpoint. Please, keep in mind that authorization endpoint and logon page are different endpoints. We could get all-known endpoints for our frontend application by visiting the url: https://lvh.me:4201/.well-known/openid-configuration in case our application uses ASP.NET Core with IdentityServer.
Next, Startup.cs of our API application should contain:
public void ConfigureServices(IServiceCollection services)
{
// ... some your code ...
services.AddSwaggerDocumentation();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication("Bearer", options =>
{
options.ApiName = "combitimeapi";
options.Authority = "https://lvh.me:4201";
});
// ... some your code ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... some your code ...
app.UseSwaggerDocumentation();
app.UseRouting();
app.UseAuthorization();
// ... some your code ...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Please, do not forget to add attribute [Authorize] to all your controllers, because your AuthorizeOperationFilter assumes that's done.
Let's look for required changes for our frontend & authorize part. You should configure some certain things, like:
CORS policy
Awailable API clients (one is your Angular UI and another one is API application)
Awailable API resources
Authentication & authorization methods
The class Startup.cs should contain:
public void ConfigureServices(IServiceCollection services)
{
// ... some your code ...
services.AddCors(policies => {
policies.AddDefaultPolicy(builder => {
builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
});
});
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.Clients.AddIdentityServerSPA("forntend", cfg => {});
options.Clients.AddNativeApp("combitimeapi_swagger", cfg => {
cfg
.WithRedirectUri("https://lvh.me:5001/oauth2-redirect.html")
.WithScopes("combitimeapi");
});
options.ApiResources.AddApiResource("combitimeapi", cfg => {
cfg.WithScopes("combitimeapi");
});
})
.AddApiResources();
services
.AddAuthentication(
x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerJwt();
// ... some your code ...
}
I use here .AddIdentityServerJwt() instead of your's .AddJwtBearer(...) because I don't have your keys and other specific options.
The frontend application is configured to use ports 4201 for HTTPS and 4200 for HTTP, the API application is configured to use ports 5001 for HTTPS and 5000 for HTTP.
Now you can run both applications and go to the page https://lvh.me:5001/index.html and press the button 'Authorize' to get a dialog like:
Enter you secret, mark scope and press 'Authorize' and, after you authenticate yourself you will get:
If you do not get a successful result, please check log of the frontend application, usually it contains error that could help you to find out a problem.
Hope text above will help you.
If you look at the OAuth Web-site the case is described as Per-Request Customization
Per-Request Customization
Often times a developer will think that they need to be able to use a
different redirect URL on each authorization request, and will try to
change the query string parameters per request. This is not the
intended use of the redirect URL, and should not be allowed by the
authorization server. The server should reject any authorization
requests with redirect URLs that are not an exact match of a
registered URL.
If a client wishes to include request-specific data in the redirect URL, it can > instead use the “state” parameter to store data that will be included after the > user is redirected. It can either encode the data in the state parameter itself, or use the state parameter as a session ID to store the state on the server.
I hope that helps you in your quest.
Source: https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uri-registration/
There may be more than one problem with the Startup code, more properly in the AddSwaggerGen.
Configuration of the Identity Provider:
Independently of the redirect, are you able to get an access token, or are you getting some kind of error, for example in the request or in the Identity Provider itself?
Please note that the client configuration that you provide in Swagger must match the configuration in the Identity Provider. You seem to be following Scott Brady's example; we can observe that all his Swagger's startup configuration follows the information he has in the Identity Server (here).
Set the token in the calls to the API:
Moreover, even if you are getting the token, I think you are not setting it in the subsequent calls from Swagger to the API itself.
The AddSecurityDefinition and the AddSecurityRequirement or the AuthorizeOperationFilter typically mention at least one scheme with the same identifier, since the first method defines the way that Swagger is authenticating and the second/third define the way that the calls to the API are authenticated (so, they must reference each other). However, you are using different IDs in all the three methods - "OAuth2", "Bearer" and "oauth2" -, so none of them is linked.
I don't fully know your application, but I believe you could actually be using only one of the AddSecurityRequirement or the AuthorizeOperationFilter, since they are both specifying security requirements. The most important would be to reference the ID of the SecurityDefinition (in your case, "OAuth2").
Scott's example, in fact, only uses the AuthorizeCheckOperationFilter and uses the same ID for the OpenApiSecurityScheme that was previously registered in the AddSecurityDefinition - in his case, "oauth2", but any name/string could be used.
recently we have migrated our project from asp.net to asp.net core, the project was working fine in asp.net, We have followed the migration document of Microsoft for migrating "asp.net to asp.net core" and modified accordingly, unfortunately, OnRedirectToIdentityProvider method in startup class is not working on challenge call (from the controller). It would be great if someone helps me to figure out where my code is wrong. I have been looping on the same issue for a while. thanks in advance.
StartUp.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
//Add a strongly-typed options class to DI
services.Configure<AuthOptions>(Configuration.GetSection("Authentication"));
services.AddAuthentication(opt => {
opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie("MiddleWareCookie")
.AddOpenIdConnect(options => Configuration.Bind("Authentication", options));
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["Authentication:Authority"];
options.ClientId= Configuration["Authentication:ClientId"];
options.ClientSecret= Configuration["Authentication:ClientSecret"];
options.TokenValidationParameters = new TokenValidationParameters
{
// Instead of using the default validation (validating against a single issuer value, as we do in
// line of business apps), we inject our own multitenant validation logic
ValidateIssuer = false,
// If the app is meant to be accessed by entire organizations, add your issuer validation logic here.
//IssuerValidator = (issuer, securityToken, validationParameters) => {
// if (myIssuerValidationLogic(issuer)) return issuer;
//}
};
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = (context) =>
{
object obj = null;
var request = context.Request;
if (context.HttpContext.Items.TryGetValue("Authority", out obj))
{
string authority = obj as string;
if (authority != null)
{
context.ProtocolMessage.IssuerAddress = authority;
}
}
//string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
string appBaseUrl = #"https://localhost:44359/";//UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
Debug.WriteLine($"appBaseUrl: {appBaseUrl}");
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
context.ProtocolMessage.Prompt = "select_account";
context.ProtocolMessage.Resource = Configuration["Authentication:AzureResourceManagerIdentifier"];
return Task.FromResult(0);
},
OnAuthorizationCodeReceived = async (context) =>
{
var request = context.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
string tenantId = context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
//Comment
Debug.WriteLine($"tenantID: {tenantId}");
// Revisit
string signedInUserUniqueName = context.Principal.FindFirst(ClaimTypes.Name).Value.Split('#')[context.Principal.FindFirst(ClaimTypes.Name).Value.Split('#').Length - 1];
//Comment
Debug.WriteLine($"tenantID: {signedInUserUniqueName}");
var tokenCache = new ADALTokenCache(signedInUserUniqueName);
tokenCache.Clear();
// revisit
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", tenantId), tokenCache);
// var items = authContext.TokenCache.ReadItems().ToList();
// revisit
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code, new Uri(currentUri), credential);
//Tell the OIDC middleware we got the tokens, it doesn't need to do anything
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
},
OnTokenValidated = (context) => {
string issuer = context.Principal.FindFirst("iss").Value;
if (issuer != null)
{
if (!issuer.StartsWith("https://sts.windows.net/"))
throw new SecurityTokenValidationException();
}
return Task.FromResult(0);
},
OnTicketReceived = context =>
{
// If your authentication logic is based on users then add your logic here
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/Error");
context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
},
// If your application needs to do authenticate single users, add your user validation below.
//OnTokenValidated = context =>
//{
// return myUserValidationLogic(context.Ticket.Principal);
//}
};
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Subscription Controller Method
HttpContext.Items.Add("Authority", string.Format(_authOptions.Authority + "OAuth2/Authorize", _directoryId));
Dictionary<string, string> dict = new Dictionary<string, string>();
dict["prompt"] = "select_account";
var userIdentity = new ClaimsIdentity(User.Claims, "login");
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
await HttpContext.AuthenticateAsync("MiddleWareCookie");
I can see two things that explain why OnRedirectToIdentityProvider is not called based on your controller code:
you're calling the AuthenticateAsync method where your question states you're calling Challenge;
you're referring to the cookie authentication scheme (MiddleWareCookie), where I think your intent is to trigger an OpenID Connect login.
I believe you need to replace HttpContext.AuthenticateAsync("MiddleWareCookie") with HttpContext.ChallengeAsync() to have the OIDC login request be triggered.
Another potential issue I can see is that you set, in the AddAuthentication method, the DefaultScheme to CookieAuthenticationDefaults.AuthenticationScheme, but the name of your cookie authentication scheme is MiddleWareCookie. These two need to be in sync.
I'm creating an ASP.NET 5 MVC 6 app that will use Facebook/Google authentication only. I'm also trying to use the cookie middleware without the whole ASP.NET Identity -- following this article:
https://docs.asp.net/en/latest/security/authentication/cookie.html
So I started with an blank app with no authentication then added the Microsoft.AspNet.Authentication.Cookies and Microsoft.AspNet.Authentication.Facebook NuGet packages in order to have a very minimalistic approach where I don't include anything that I don't need.
I added the following code into Configure in Startup.cs but I'm getting "SignInScheme option must be provided" error. Any idea what I'm missing?
app.UseCookieAuthentication(options =>
{
options.AuthenticationScheme = "MyCookieMiddlewareInstance";
options.LoginPath = new PathString("/Accounts/Login/");
options.AccessDeniedPath = new PathString("/Error/Unauthorized/");
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
});
app.UseFacebookAuthentication(options =>
{
options.AppId = "myFacebookAppIdGoesHere";
options.AppSecret = "myFacebookAppSecretGoesHere";
});
As indicated by the error message you're seeing, you need to set options.SignInScheme in your Facebook middleware options:
app.UseFacebookAuthentication(options => {
options.AppId = "myFacebookAppIdGoesHere";
options.AppSecret = "myFacebookAppSecretGoesHere";
// This value must correspond to the instance of the cookie
// middleware used to create the authentication cookie.
options.SignInScheme = "MyCookieMiddlewareInstance";
});
Alternatively, you can also set it globally from ConfigureServices (it will configure every authentication middleware so you don't have to set options.SignInScheme):
public void ConfigureServices(IServiceCollection services) {
services.AddAuthentication(options => {
// This value must correspond to the instance of the cookie
// middleware used to create the authentication cookie.
options.SignInScheme = "MyCookieMiddlewareInstance";
});
}