Configuring token in azure functions - c#

Problem is that I get Functions runtime is unreachable error after adding AddAccessTokenManagement() in startup.cs file. Also the list of fuctions in azure is empty. The best part is that from app insights I see that my cron job is beeing executed anyway, and token is working. When running my code in local enviroment there is no problem reported, deployments also seems to be fine. This is how I configure my http client to work with identity token:
private void ConfigureAccessToken(IFunctionsHostBuilder builder)
{
var IdentityServerUrl = "<serverUrl>"; ;
builder.Services.AddHttpClient();
builder.Services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("cloud-service", new ClientCredentialsTokenRequest
{
Address = $"{IdentityServerUrl}/connect/token",
ClientId = _authorizationConfig.ClientId,
ClientSecret = _authorizationConfig.ClientSecret,
});
});
builder.Services.AddClientAccessTokenClient("internal-client", configureClient: client => { });
}
Worth to mention that this way of configuring it works with my Web API application.
Any ideas guys?

I found the answer by myself. Looks like token confiuration for azure functions differ from Web API. Working code below:
private void ConfigureAccessToken(IFunctionsHostBuilder builder)
{
var IdentityServerUrl = "<serverUri>";
builder.Services.Configure<AccessTokenManagementOptions>(o =>
{
o.Client.Clients.Add("cloud-service", new ClientCredentialsTokenRequest
{
Address = $"{IdentityServerUrl}/connect/token",
ClientId = _authorizationConfig.ClientId,
ClientSecret = _authorizationConfig.ClientSecret,
});
});
builder.Services.AddDistributedMemoryCache();
builder.Services.AddTransient<ITokenClientConfigurationService, DefaultTokenClientConfigurationService>(s =>
{
return new DefaultTokenClientConfigurationService(
s.GetRequiredService<IOptions<AccessTokenManagementOptions>>(),
null,
null);
});
builder.Services.AddHttpClient(AccessTokenManagementDefaults.BackChannelHttpClientName);
builder.Services.TryAddTransient<ITokenEndpointService, TokenEndpointService>();
builder.Services.TryAddTransient<IClientAccessTokenCache, ClientAccessTokenCache>();
builder.Services.AddTransient<IAccessTokenManagementService, AccessTokenManagementService>(s =>
{
return new AccessTokenManagementService(
null,
null,
s.GetRequiredService<IOptions<AccessTokenManagementOptions>>(),
s.GetRequiredService<ITokenEndpointService>(),
s.GetRequiredService<IClientAccessTokenCache>(),
s.GetRequiredService<ILogger<AccessTokenManagementService>>()
);
});
builder.Services.AddTransient<ClientAccessTokenHandler>();
builder.Services.AddClientAccessTokenClient("internal-client", configureClient: config => {});
}

Related

ClaimsIdentity cant access methods in official OpenIdDict Samples

I have been very frustrated trying to use openiddict. I can't use any of the pre-existing sample since their ClaimsIdentity uses methods that to me aren't available, for example the identity.SetClaims(), identity.SetScopes() and identity.GetScopes() don't work for me.
This is the official sample Zirku.Server:
var builder = WebApplication.CreateBuilder(args);
// OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
// (like pruning orphaned authorizations/tokens from the database) at regular intervals.
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddDbContext<DbContext>(options =>
{
// Configure the context to use Microsoft SQL Server.
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, introspection and token endpoints.
options.SetAuthorizationEndpointUris("/authorize")
.SetIntrospectionEndpointUris("/introspect")
.SetTokenEndpointUris("/token");
// Note: this sample only uses the authorization code flow but you can enable
// the other flows if you need to support implicit, password or client credentials.
options.AllowAuthorizationCodeFlow();
// Register the signing credentials.
options.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
//
// Note: unlike other samples, this sample doesn't use token endpoint pass-through
// to handle token requests in a custom MVC action. As such, the token requests
// will be automatically handled by OpenIddict, that will reuse the identity
// resolved from the authorization code to produce access and identity tokens.
//
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough();
})
// 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.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
// Create new application registrations matching the values configured in Zirku.Client and Zirku.Api1.
// Note: in a real world application, this step should be part of a setup script.
await using (var scope = app.Services.CreateAsyncScope())
{
var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync();
await CreateApplicationsAsync();
await CreateScopesAsync();
async Task CreateApplicationsAsync()
{
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("console_app") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "console_app",
RedirectUris =
{
new Uri("http://localhost:8739/")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "api1",
Permissions.Prefixes.Scope + "api2"
}
});
}
if (await manager.FindByClientIdAsync("resource_server_1") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "resource_server_1",
ClientSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342",
Permissions =
{
Permissions.Endpoints.Introspection
}
});
}
// Note: no client registration is created for resource_server_2
// as it uses local token validation instead of introspection.
}
async Task CreateScopesAsync()
{
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
if (await manager.FindByNameAsync("api1") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api1",
Resources =
{
"resource_server_1"
}
});
}
if (await manager.FindByNameAsync("api2") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api2",
Resources =
{
"resource_server_2"
}
});
}
}
}
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api", [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
(ClaimsPrincipal user) => user.Identity!.Name);
app.MapGet("/authorize", async (HttpContext context, IOpenIddictScopeManager manager) =>
{
// Retrieve the OpenIddict server request from the HTTP context.
var request = context.GetOpenIddictServerRequest();
var identifier = (int?)request["hardcoded_identity_id"];
if (identifier is not (1 or 2))
{
return Results.Challenge(
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidRequest,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified hardcoded identity is invalid."
}));
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.AddClaim(new Claim(Claims.Subject, identifier.Value.ToString(CultureInfo.InvariantCulture)));
identity.AddClaim(new Claim(Claims.Name, identifier switch
{
1 => "Alice",
2 => "Bob",
_ => throw new InvalidOperationException()
}));
// Note: in this sample, the client is granted all the requested scopes for the first identity (Alice)
// but for the second one (Bob), only the "api1" scope can be granted, which will cause requests sent
// to Zirku.Api2 on behalf of Bob to be automatically rejected by the OpenIddict validation handler,
// as the access token representing Bob won't contain the "resource_server_2" audience required by Api2.
identity.SetScopes(identifier switch
{
1 => request.GetScopes(),
2 => new[] { "api1" }.Intersect(request.GetScopes()),
_ => throw new InvalidOperationException()
});
identity.SetResources(await manager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Allow all claims to be added in the access tokens.
identity.SetDestinations(claim => new[] { Destinations.AccessToken });
return Results.SignIn(new ClaimsPrincipal(identity), properties: null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
});
app.Run();
I have been trying to convert the samples to identity.AddClaim(), but I am not sure that it works as expected.
Please tell me what I am doing wrong. I am new to authorization and authentication so, as you can imagine, I am not good enough to figure out what is going wrong.
PS If you have any good up-to-date sources so that I can read up on the subject, that would be great.
PS 2 Excuse my english, it is not my first language

How to use other Singleton from within builder.Services.Configure() in C#

I'm building a .NET 7 MVC app that uses Azure AD for Authentication but calls out to another API to add additional claims to the Identity.
This worked great when I defined the Claim Transformation statically, but I'd like to register the Claim Transformation as a singleton instead so that it can manage its own token lifetime to the API.
This is what the code looked like to add the claims when the transformation was static:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
}
};
});
This works, but the Claim Transformation class can't store a bearer jwt, and would need to get a fresh one every time, wasting a ton of resources.
this is the closest I've come to getting it to work as a singleton, but it causes plenty of issues
builder.Services.AddSingleton<ICLaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await builder.Services.BuildServiceProvider()
.GetRequiredService<IClaimsAPI>()
.TransformAsync(context.Principal);
}
};
});
This generates a seperate copy of each singleton, which doesn't really work for obvious reasons.
How can I inject my service so that it adds the claims correctly?
EDIT: Solved!
I had to do some slight tweaks to #Acegambit's code. here is my working solution for postierity, just in case someone in the future needs to solve a similar problem.
builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddOptions().AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
var ClaimsAPI = provider.GetRequiredService<IClaimsAPI>();
return new ConfigureNamedOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)
{
context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
}
};
});
});
This took a little digging into the IServiceCollection extension methods. Looking at the implementation of Configure<TOptions> it really doesn't do a whole lot other than call .AddOptions() and register a singleton of type IConfigureOptions so I think you can pull out that code and do it yourself like so:
builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddOptions();
builder.Services.AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
var claimsApi = provider.GetRequiredService<IClaimsAPI>();
return new ConfigureNamedOptions<MicrosoftIdentityOptions>(string.Empty, options =>
{
// TODO: insert your logic to set the context.Principle here
// using the claimsApi that should resolve from the provider above
});
});
There's already an answer but I figure it would be good to show how options has evolved to make this scenario a bit more terse:
builder.Services.AddOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme)
.Configure<IClaimsAPI>((options, claimsApi) =>
{
options.Events = new()
{
OnTokenValidated = context =>
{
context.Principal = claimsApi.Transform(context.Principal);
return Task.CompletedTask;
}
};
});

How to configure json Name Case policy while using MinimalApi

I'm trying to get result from my minimal API who configured in endpoints of my MVC web application
my Get action configured like this :
endpoints.MapGet(
"HO-CFDZU4/api/Currency/Get",
[PermissionAuthorize(PermissionName.ReadCurrencyDictionary)]
async ([FromServicesAttribute] CurrencyService curency) =>
{
var result = await DataSourceLoader.LoadAsync(curency.Get(), new DataSourceLoadOptions());
return Results.Ok(result);
});
As result i get response with object where property names changed to lowercase, and its not suit for me.
I want to get exactly same name in same case like i return form action.
To get similar effect in MVC i used this code :
services
.AddMvc()
.AddFluentValidation(x => x.RegisterValidatorsFromAssembly(AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName.Contains("ApplicationCore")).Single()))
.AddMvcLocalization()
.AddMvcOptions(options =>{})
.AddRazorRuntimeCompilation()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
Which setup property naming policy for Json while using action in controllers, and i dont know how to setup same policy for minimalApi.
What Ive tried is to set [JsonPropertyName(name)] And it working good but we have lot of classes and i looking for more global solution.
I also tried configure JsonOptions globally like this:
services.Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
But it do nothing
Use JsonOptions from Microsoft.AspNetCore.Http.Json namespace (docs):
services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
UPD
If your application uses both Minimal APIs endpoints and MVC ones, then you try to configure options from both namespaces:
services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
services.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});

C# Blazor server Azure AD auth setup with Web API and graph token

i have an issue with my c# blazor server setup.
I am fairly new to the .net eco system and I have a problem I can not figure out on my own. I hope any of you can help me.
I have a blazor server setup with Azure AD Authentication:
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
When the user has autheticated, I can get an access token with the following code:
var token = new TokenProvider
{
AccessToken = await HttpContext.GetTokenAsync("access_token")
};
I use this token to call the Microsoft Web API to fetch customers from Dynamics CRM, with the following setup:
// Setup client
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme,
opt =>
{
var resourceUri = new Uri(Configuration["CDSAPI"]);
var resource = $"{resourceUri.Scheme}://{resourceUri.Host}/";
opt.ResponseType = "code";
opt.SaveTokens = true;
opt.Scope.Add("user_impersonation");
opt.Scope.Add(new Uri(Configuration["CDSAPI"]).Host);
opt.Resource = resource;
});
services.AddScoped<TokenProvider>();
services.AddHttpClient("CDS", client =>
{
client.BaseAddress = new Uri(Configuration["CDSAPI"]);
});
I then use this client in my code to fetch the customers:
try
{
var client = Factory.CreateClient("CDS");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TokenProvider.AccessToken);
var result = await client.GetStringAsync("accounts?$select=name&$orderby=name");
DynamicsCustomerEntity customerCollection = JsonConvert.DeserializeObject<DynamicsCustomerEntity>(result);
customers = customerCollection.value;
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
finally
{
}
This works fine. But the issue presents itself when I try to implement Microsoft Graph functionality.
For example, if I try to use this code:
services.AddMicrosoftWebAppAuthentication(Configuration)
.AddMicrosoftWebAppCallsWebApi(Configuration, new string[] { "Calendars.Read", "Calendars.ReadWrite" })
.AddDistributedTokenCaches();
services.AddHttpClient();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
instead of
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
I can no longer access the token with
await HttpContext.GetTokenAsync("access_token")
So my question is:
How can I make a blazor server setup to get two valid tokens - one for Graph API calls and one for Microsoft Web API calls? I need delegated permission tokens for both.
I hope my question makes sence. I have tried to provide as much context as possible.
Thanks in advance.

OAuth Implementation in ASP.NET Core using Swagger

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.

Categories

Resources