ASP.NET OAuth授权-使用ClientId和Secret与使用用户名和密码的区别

7
我正在尝试在ASP.NET WebAPI 2中实现一个简单的OAuthAuthorizationServerProvider。我的主要目的是学习如何为移动应用程序生成令牌。我希望用户使用用户名和密码登录,然后接收一个令牌(以及一个刷新令牌,这样他们就不必在令牌过期后重新输入凭据)。稍后,我希望有机会向其他应用程序(比如使用Facebook api等)开放API。
以下是我设置AuthorizationServer的方式:
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions()
{
    AllowInsecureHttp = true,
    TokenEndpointPath = new PathString("/token"),
    AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),
    Provider = new SimpleAuthorizationServerProvider(new SimpleAuthorizationServerProviderOptions()
    {
        ValidateUserCredentialsFunction = ValidateUser
    }),
    RefreshTokenProvider = new SimpleRefreshTokenProvider()
});

这是我的SimpleAuthorizationServerProviderOptions实现:
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public delegate Task<bool> ClientCredentialsValidationFunction(string clientid, string secret);
    public delegate Task<IEnumerable<Claim>> UserCredentialValidationFunction(string username, string password);
    public SimpleAuthorizationServerProviderOptions Options { get; private set; }

    public SimpleAuthorizationServerProvider(SimpleAuthorizationServerProviderOptions options)
    {
        if (options.ValidateUserCredentialsFunction == null)
        {
            throw new NullReferenceException("ValidateUserCredentialsFunction cannot be null");
        }
        Options = options;
    }

    public SimpleAuthorizationServerProvider(UserCredentialValidationFunction userCredentialValidationFunction)
    {
        Options = new SimpleAuthorizationServerProviderOptions()
        {
            ValidateUserCredentialsFunction = userCredentialValidationFunction
        };
    }

    public SimpleAuthorizationServerProvider(UserCredentialValidationFunction userCredentialValidationFunction, ClientCredentialsValidationFunction clientCredentialsValidationFunction)
    {
        Options = new SimpleAuthorizationServerProviderOptions()
        {
            ValidateUserCredentialsFunction = userCredentialValidationFunction,
            ValidateClientCredentialsFunction = clientCredentialsValidationFunction
        };
    }

    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        if (Options.ValidateClientCredentialsFunction != null)
        {
            string clientId, clientSecret;

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

            var clientValidated = await Options.ValidateClientCredentialsFunction(clientId, clientSecret);
            if (!clientValidated)
            {
                context.Rejected();
                return;
            }
        }

        context.Validated();
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (Options.ValidateUserCredentialsFunction == null)
        {
            throw new NullReferenceException("ValidateUserCredentialsFunction cannot be null");
        }

        var claims = await Options.ValidateUserCredentialsFunction(context.UserName, context.Password);
        if (claims == null)
        {
            context.Rejected();
            return;
        }

        // create identity
        var identity = new ClaimsIdentity(claims, context.Options.AuthenticationType);

        // create metadata to pass to refresh token provider
        var props = new AuthenticationProperties(new Dictionary<string, string>()
        {
            { "as:client_id", context.UserName }
        });

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

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

        // enforce client binding of refresh token
        if (originalClient != currentClient)
        {
            context.Rejected();
            return;
        }

        // chance to change authentication ticket for refresh token requests
        var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
        newIdentity.AddClaim(new Claim("newClaim", "refreshToken"));

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

这是我的SimpleRefreshTokenProvider实现:

public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
    private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens =
        new ConcurrentDictionary<string, AuthenticationTicket>(); 

    public void Create(AuthenticationTokenCreateContext context)
    {

    }

    public async Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var guid = Guid.NewGuid().ToString();

        var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
        {
            IssuedUtc = context.Ticket.Properties.IssuedUtc,
            ExpiresUtc = DateTime.UtcNow.AddYears(1)
        };
        var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

        _refreshTokens.TryAdd(guid, refreshTokenTicket);
        context.SetToken(guid);
    }

    public void Receive(AuthenticationTokenReceiveContext context)
    {

    }

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        AuthenticationTicket ticket;
        if (_refreshTokens.TryRemove(context.Token, out ticket))
        {
            context.SetTicket(ticket);
        }
    }
}

我不完全理解ClientId和SecretUsername和Password的使用区别。我粘贴的代码通过用户名和密码生成一个令牌,我可以使用该令牌(直到过期),但是当我尝试获取刷新令牌时,必须拥有ClientId。此外,如果令牌过期,正确的方法是发送刷新令牌并获取新令牌吗?如果刷新令牌被盗,那怎么办呢?这难道不就像用户名和密码被盗一样吗?
1个回答

5
我不完全明白使用ClientId和SecretUsername和Password的区别。我粘贴的代码通过用户名和密码生成一个令牌,我可以使用该令牌(直到它过期),但是当我尝试获取刷新令牌时,必须要有ClientId。
另外,如果令牌过期,正确的方法是发送刷新令牌并获取新令牌吗?如果刷新令牌被盗了怎么办?这不是和用户名和密码被盗一样吗?
在OAuth2中,对于协议定义的任何授权流程,都必须对用户和客户端进行身份验证。客户端身份验证(正如您可能猜到的那样)强制使用API的已知客户端。生成序列化访问令牌后,该令牌不直接绑定到特定客户端。请注意,ClientSecret必须被视为机密信息,并且只能由可以以某种安全方式存储此信息的客户端使用(例如外部服务客户端,但不包括javascript客户端)。
刷新令牌只是OAuth2的另一种“授权类型”,正如您正确指出的那样,它将替换用户的用户名和密码对。该令牌必须被视为机密数据(比访问令牌更机密),但是相对于在客户端上存储用户名和密码,它具有以下优点:
  • 如果被攻击者获取,用户可以撤销它;
  • 它有一个有限的生命周期(通常为几天或几周);
  • 它不会暴露用户凭据(攻击者只能获取发出刷新令牌的“范围”的访问令牌)。
我建议您阅读有关OAuth 2中定义的不同授权类型的更多信息,请查看官方草案。当我自己首次在Web API中实现OAuth2时,我还推荐您查看这个资源,我认为它非常有用。 示例请求 以下是使用Fiddler的两个请求示例,用于Resource Owner Password Credentials GrantFiddler Request: Resource Owner Grant 以及Refresh Token Grantenter image description here

谢谢!还发现了这篇有用的文章:http://leastprivilege.com/2013/11/15/adding-refresh-tokens-to-a-web-api-v2-authorization-server/ - developer82
leastprivilege 的所有东西都是金子般的。如果将来您需要完整的 OAuth2 和 OpenID 实现,我只能建议您尝试使用 IdentityServer - Federico Dipuma
另一个问题...如果我理解正确的话,即使是一个简单的实现,我也需要至少有一个“客户端”(我自己)-所以现在我需要验证客户端和用户吗?在两个不同的调用中向服务器发送用户名和密码以及客户端ID和密钥是否可行? - developer82
您是正确的,至少需要一个客户端。OWIN中间件将在每个令牌请求时自动验证客户端和用户凭据(内部调用ValidateClientAuthentication,如果有效,则调用所选授权类型的正确验证)。 - Federico Dipuma
你能给我演示一下在Fiddler中需要输入什么来实现这个吗? - developer82
抱歉回复晚了,刚刚编辑了答案并包含了两个基本示例。 - Federico Dipuma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接