Как реализовать OpenID OAuth2 Server в веб-API ASP.NET Framework 4.x? - PullRequest
0 голосов
/ 20 марта 2019

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

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

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

Я следовал замечательным учебникам Taiseer ( Часть 1 , Часть 3 , Part 5 и JWT Setup ), и в настоящее время у него есть сервер 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://localhost:7814/oauth2/token со следующими параметрами:

grant_type = пароль

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

пароль = p @ ssw0rd

client_id = smLocalhost

client_secret = secret

scope = api1 api2

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

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

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

1.Как добавить OpenID поверх этого?

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

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

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

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

...