Как реализовать сервер OpenID OAuth2 в веб-API ASP.NET Framework 4.x?

Я пытаюсь реализовать сервер OpenID OAuth 2.0 с веб-API ASP.NET Framework 4.7.2. Он будет использоваться для защиты API-интерфейсов ресурсов с помощью токенов JWT Access/Refresh.

Я новичок в OpenID и OAuth, поэтому я ищу советы/рекомендации/библиотеки, которые я могу использовать для реализации этого сервера авторизации.

Сервер аутентификации должен быть реализован с использованием ASP.NET Framework 4.7.2, для Core на данный момент нет возможности. API-интерфейсы ресурсов будут написаны на ASP.NET Core 2.X.

Я следовал замечательным руководствам Taiseer (Часть 1, Часть 3, Часть 5 и Настройка JWT), и в настоящее время у меня есть сервер OAuth, который может генерировать токены JWT, и Core API, который может проверять токен.

Вот код, который у меня сейчас есть.

Сервер авторизации:

Startup.cs

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureOAuth(app);

        HttpConfiguration config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);
    }

    public void ConfigureOAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext<SecurityUserManager>(SecurityUserManager.Create);

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true, 
            TokenEndpointPath = new PathString("/oauth2/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30), 
            Provider = new SmAuthorizationServerProvider(),
            RefreshTokenProvider = new SmRefreshTokenProvider(),
            AccessTokenFormat = new SmJwtFormat("http://localhost:7814"),
            ApplicationCanDisplayErrors = true,
        };

        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
    }
}

SmAuthorizationServerProvider.cs

public class SmAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        string clientId = string.Empty;
        string clientSecret = string.Empty;
        Client client = null;

        if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
        {
            context.TryGetFormCredentials(out clientId, out clientSecret);
        }

        if (context.ClientId == null)
        {
            //Remove the comments from the below line context.SetError, and invalidate context 
            //if you want to force sending clientId/secrects once obtain access tokens. 
            context.Validated();
            //context.SetError("invalid_clientId", "ClientId should be sent.");
            return Task.FromResult<object>(null);
        }

        using (AuthRepository _repo = new AuthRepository())
        {
            client = _repo.FindClient(context.ClientId);
        }

        if (client == null)
        {
            context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
            return Task.FromResult<object>(null);
        }

        if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential)
        {
            if (string.IsNullOrWhiteSpace(clientSecret))
            {
                context.SetError("invalid_clientId", "Client secret should be sent.");
                return Task.FromResult<object>(null);
            }
            else
            {
                if (client.Secret != Helper.GetHash(clientSecret))
                {
                    context.SetError("invalid_clientId", "Client secret is invalid.");
                    return Task.FromResult<object>(null);
                }
            }
        }

        if (!client.Active)
        {
            context.SetError("invalid_clientId", "Client is inactive.");
            return Task.FromResult<object>(null);
        }

        context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
        context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());

        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
        SecurityUser user = null;

        if (allowedOrigin == null) allowedOrigin = "*";

        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });

        using (AuthRepository _repo = new AuthRepository())
        {
            user = await _repo.FindUser(context.UserName, context.Password);

            if (user == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
        }

        string scopes = null;

        if (context.Scope.Count > 0)
        {
            scopes = string.Join(" ", context.Scope.Select(x => x.ToString()).ToArray());
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);

        // Add the user id as claim here 
        // Keep the claims number small, the token length increases with each new claim
        identity.AddClaim(new Claim("sid", user.Id.ToString()));

        // add the client id as claim 
        if (!string.IsNullOrEmpty(context.ClientId))
        {
            identity.AddClaim(new Claim("client_id", context.ClientId));
        }

        var props = new AuthenticationProperties(new Dictionary<string, string>
        {
            {
                "as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
            },
            {
                "as:scope", (scopes == null) ? string.Empty : scopes
            },
            {
                "userName", context.UserName 
            }
        });


        var ticket = new AuthenticationTicket(identity, props);
        context.Validated(ticket);
    }

    public override Task TokenEndpoint(OAuthTokenEndpointContext context)
    {
        foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
        {
            context.AdditionalResponseParameters.Add(property.Key, property.Value);
        }

        return Task.FromResult<object>(null);
    }

    public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
    {
        var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
        var currentClient = context.ClientId;

        // check if the token is created with specified client_id
        if (!string.IsNullOrEmpty(currentClient) && !string.IsNullOrEmpty(originalClient))
        {
            if (originalClient != currentClient)
            {
                context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
                return Task.FromResult<object>(null);
            }
        }

        var newIdentity = new ClaimsIdentity(context.Ticket.Identity);

        // Change auth ticket for refresh token requests if needed
        // newIdentity.AddClaim(new Claim("newClaim", "newValue"));

        var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
        context.Validated(newTicket);

        return Task.FromResult<object>(null);
    }

    public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
    {
        Client client; 

        using (AuthRepository _repo = new AuthRepository())
        {
            client = _repo.FindClient(context.ClientId);
        }

        var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
        oAuthIdentity.AddClaim(new Claim("client_id", client.Id));

        var props = new AuthenticationProperties(new Dictionary<string, string>
        {
            {
                "as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
            }
        });

        var ticket = new AuthenticationTicket(oAuthIdentity, props);
        context.Validated(ticket);
        return base.GrantClientCredentials(context);
    }

}

SmJwtFormat.cs

public class SmJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
    private const string AudiencePropertyKey = "as:scope";
    private readonly string _issuer = string.Empty;
    private AuthRepository authRepo;
    private SecurityKey signingKey;

    private string secret = "P@ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";

    public SmJwtFormat(string issuer)
    {
        _issuer = issuer;
        authRepo = new AuthRepository();

        // Generating the signingKey
        string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));

        var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);

        signingKey = new SymmetricSecurityKey(keyByteArray);
    }

    public string Protect(AuthenticationTicket data)
    {
        if (data == null)
        {
            throw new ArgumentNullException("data");
        }

        // The token audience from the JWT terminology is the same as the token Scope in OAuth terminology. 
        string scope = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? data.Properties.Dictionary[AudiencePropertyKey] : null;

        if (string.IsNullOrWhiteSpace(scope)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience/scope");

        var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature);

        var issued = data.Properties.IssuedUtc;
        var expires = data.Properties.ExpiresUtc;

        if (scope != null)
        {
            var scopesList = scope.Split(' ').ToList();

            var audClaims = scopesList.Select(s => new Claim("aud", s));

            data.Identity.AddClaims(audClaims);
        }

        var token = new JwtSecurityToken(_issuer, null, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingCredentials);


        var handler = new JwtSecurityTokenHandler();

        var jwt = handler.WriteToken(token);

        return jwt;
    }

    public AuthenticationTicket Unprotect(string protectedText)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = _issuer,
            IssuerSigningKey = signingKey,

        };

        var handler = new JwtSecurityTokenHandler();
        SecurityToken token = null;

        // Unpack token
        var pt = handler.ReadJwtToken(protectedText);
        string t = pt.RawData;

        var principal = handler.ValidateToken(t, tokenValidationParameters, out token);

        var identity = principal.Identities;

        return new AuthenticationTicket(identity.First(), new AuthenticationProperties());
    }
}

Вот мой ресурс ASP.NET Core 2.2 API Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddAuthorization();

        // identity http://localhost:7814
        // resource https://localhost:44337

        var key = "P@ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
        string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(key));
        var keyByteArray = Convert.FromBase64String(symmetricKeyAsBase64);
        var securityKey = new SymmetricSecurityKey(keyByteArray);

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

        }).AddJwtBearer(o =>
        {
            o.Authority = "http://localhost:7814/";
            o.RequireHttpsMetadata = false;

            o.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidateIssuer = true,
                ValidIssuer = "http://localhost:7814",
                ValidateAudience = true,
                ValidAudiences = new List<string>()
                {
                    "api1" 
                },

                //IssuerSigningKey = securityKey
            };
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseAuthentication();

        app.UseMvc();
    }
}

В настоящее время я могу генерировать токены, публикуя http://локальный:7814/oauth2/токен со следующими параметрами:

грант_тип = пароль

имя пользователя=пользователь1

пароль=p@ssw0rd

client_id = smLocalhost

client_secret = секрет

область = API1 API2

После этого токен можно использовать для доступа к защищенным конечным точкам в ресурсном API.

Я исследовал ИдентитиСервер4, OpenIdDict и AspNet.Security.OpenIdConnect.Server, но похоже, что они работают только с ASP.NET Core.

Итак, после всего этого материала, мои вопросы:

1. Как добавить к этому OpenID?

2. Есть ли библиотека, которую я могу использовать?

3. Не могли бы вы дать мне руководство/совет, что я могу сделать для реализации Это?

4. Как реализовать конечную точку документа обнаружения (.well-known/openid-configuration) и чередовать открытые ключи для подписи асимметричного токена?

Заранее спасибо!

С вашим предоставленным кодом, как вы вообще можете получить доступ к защищенным конечным точкам в API ресурсов (в основном приложении .net), используя сгенерированный токен (в приложении .net framework)? Я попытался использовать токен для доступа к ресурсному API, и он не смог этого сделать (поскольку файлы .well-known/openid-configuration не находятся в приложении .net framework).

Patrick 26.02.2020 19:27

Хорошо, я понял. Я просто удаляю o.Authority = "http://localhost:7814/"; в ресурсе ASP.NET Core 2.2 API Startup.cs и раскомментирую IssuerSigningKey = securityKey, и теперь он работает. В любом случае, я надеюсь, что на 4 вопроса можно будет ответить.

Patrick 26.02.2020 19:46

1. У проекта вы указали являются OpenID. 2. Следуйте инструкциям в проектах по установке. 3. Попробуйте спросить в ServerFault, а не в StackOverflow. 4. Опубликуйте, что вы пробовали, и проблему, которую вы пытаетесь исправить.

Suncat2000 11.06.2021 15:04
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
3
2 109
0

Другие вопросы по теме