Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 569
Simran Kaur Avatar asked Mar 12 '26 20:03

Simran Kaur


2 Answers

First, let me add some details to your picture:

  1. 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.
  2. You use .NETCore 3.1
  3. 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:

  1. CORS policy
  2. Awailable API clients (one is your Angular UI and another one is API application)
  3. Awailable API resources
  4. 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: auth dialog

Enter you secret, mark scope and press 'Authorize' and, after you authenticate yourself you will get: auth dialog

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.

like image 101
Igor Goyda Avatar answered Mar 15 '26 09:03

Igor Goyda


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/

like image 40
xcskilab Avatar answered Mar 15 '26 11:03

xcskilab



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!