[Switch] ASP.NET WebApi OWIN implemented OAuth 2.0

OAuth (Open Authorization) is an open standard that allows users to allow third-party applications to access private resources (such as photos, videos, contact lists) stored in the user on a website without having to provide user name and password to a third party application.

OAuth allows users to provide a token instead of a user name and password to access their data stored in a particular service provider. Each token is authorized for a particular site (for example, video editing website) (for example, within the next two hours) access to specific resources within a specific period of time (for example, just a video album). In this way, OAuth allows users to authorize third-party website to access certain information they store in another service provider, not all content.


The concept from above: https://zh.wikipedia.org/wiki/OAuth

What OAuth is? Why use OAuth? The above concept has been very clear, not described in detail here.

Read catalog:

  • Running processes and licensing models
  • Authorization code pattern (authorization code)
  • Simplified mode (implicit grant type)
  • Password mode (resource owner password credentials)
  • Client mode (Client Credentials Grant)

Open Source Address: https://github.com/yuezhongxin/OAuth2.Demo

1. Run the process and licensing models

Run on OAuth 2.0 flow (from RFC 6749 ):

Here we simulate a scenario: the user to listen to the arrest, but need to be logged in to collection of journals, and then use the quick login after use microblogging account and password, the arrest will have access to account information microblogging, etc., and the arrest has also been login, users can finally a collection of journals.

Combination of the above scenarios, detailed operating under the OAuth 2.0 flow:

  • (A) a user logs caught, arrested query request of the user login authorization (the real action is to log the user in the arrest).
  • (B) the user agrees to login authorization (real operation is a user opens a quick login, the user enters a microblogging account and password).
  • (C) were arrested by the authorization page jump to micro-blog, and request for authorization (microblogging account and password where required).
  • (D) to validate user input microblogging account and password, if successful, will be returned to the access_token arrested.
  • (E) to get caught access_token returned request microblogging.
  • (F) micro-blog verification access_token arrested offer, if successful, microblogging account information will be returned to the arrest.

Figure Glossary:

  • Client -> arrested
  • Resource Owner -> Users
  • Authorization Server -> Weibo Authorized Service
  • Resource Server -> microblogging service resources

In fact, I do not really understand the ABC operation, I think ABC can synthesize a C: Open Authorization arrested microblogging page, the user input microblogging account and password, request verification.

OAuth 2.0 Authorization four kinds of modes:

  • Authorization code pattern (authorization code)
  • Simplified mode (Implicit)
  • Password mode (resource owner password credentials)
  • Client mode (client credentials)

Here we use the ASP.NET WebApi OWIN, respectively, to achieve the above four licensing model.

2. authorization code pattern (authorization code)

Simple explanation: the arrest provide authorization credentials, obtain authorization from the microblogging service to authorization_code, then according to authorization_code, then get to the access_token, arrest need to request authorization microblogging service twice.

The first request authorization service (get authorization_code), the required parameters:

  • grant_type: Required, authorization mode, the value "authorization_code".
  • response_type: Required, license type, the value is fixed to "code".
  • client_id: Required, the client ID.
  • redirect_uri:必选,重定向 URI,URL 中会包含 authorization_code。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。
  • state:可选,客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值,比如微博授权服务值为 weibo。

第二次请求授权服务(获取 access_token),需要的参数:

  • grant_type:必选,授权模式,值为 "authorization_code"。
  • code:必选,授权码,值为上面请求返回的 authorization_code。
  • redirect_uri:必选,重定向 URI,必须和上面请求的 redirect_uri 值一样。
  • client_id:必选,客户端 ID。

第二次请求授权服务(获取 access_token),返回的参数:

  • access_token:访问令牌.
  • token_type:令牌类型,值一般为 "bearer"。
  • expires_in:过期时间,单位为秒。
  • refresh_token:更新令牌,用来获取下一次的访问令牌。
  • scope:权限范围。

ASP.NET WebApi OWIN 需要安装的程序包:

  • Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.OAuth
  • Microsoft.Owin.Security.Cookies
  • Microsoft.AspNet.Identity.Owin

在项目中创建 Startup.cs 文件,添加如下代码:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        var OAuthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            AuthenticationMode = AuthenticationMode.Active,
            TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
            AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
            AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间

            Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
            AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(), //authorization_code 授权服务
            RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
        };
        app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
    }
}

OpenAuthorizationServerProvider 示例代码:

public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    /// <summary>
    /// 验证 client 信息
    /// </summary>
    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        string clientId;
        string clientSecret;
        if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
        {
            context.TryGetFormCredentials(out clientId, out clientSecret);
        }

        if (clientId != "xishuai")
        {
            context.SetError("invalid_client", "client is not valid");
            return;
        }
        context.Validated();
    }

    /// <summary>
    /// 生成 authorization_code(authorization code 授权方式)、生成 access_token (implicit 授权模式)
    /// </summary>
    public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
    {
        if (context.AuthorizeRequest.IsImplicitGrantType)
        {
            //implicit 授权方式
            var identity = new ClaimsIdentity("Bearer");
            context.OwinContext.Authentication.SignIn(identity);
            context.RequestCompleted();
        }
        else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
        {
            //authorization code 授权方式
            var redirectUri = context.Request.Query["redirect_uri"];
            var clientId = context.Request.Query["client_id"];
            var identity = new ClaimsIdentity(new GenericIdentity(
                clientId, OAuthDefaults.AuthenticationType));

            var authorizeCodeContext = new AuthenticationTokenCreateContext(
                context.OwinContext,
                context.Options.AuthorizationCodeFormat,
                new AuthenticationTicket(
                    identity,
                    new AuthenticationProperties(new Dictionary<string, string>
                    {
                        {"client_id", clientId},
                        {"redirect_uri", redirectUri}
                    })
                    {
                        IssuedUtc = DateTimeOffset.UtcNow,
                        ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AuthorizationCodeExpireTimeSpan)
                    }));

            await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
            context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
            context.RequestCompleted();
        }
    }

    /// <summary>
    /// 验证 authorization_code 的请求
    /// </summary>
    public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
    {
        if (context.AuthorizeRequest.ClientId == "xishuai" && 
            (context.AuthorizeRequest.IsAuthorizationCodeGrantType || context.AuthorizeRequest.IsImplicitGrantType))
        {
            context.Validated();
        }
        else
        {
            context.Rejected();
        }
    }

    /// <summary>
    /// 验证 redirect_uri
    /// </summary>
    public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
    {
        context.Validated(context.RedirectUri);
    }

    /// <summary>
    /// 验证 access_token 的请求
    /// </summary>
    public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
    {
        if (context.TokenRequest.IsAuthorizationCodeGrantType || context.TokenRequest.IsRefreshTokenGrantType)
        {
            context.Validated();
        }
        else
        {
            context.Rejected();
        }
    }
}

需要注意的是,ValidateClientAuthentication 并不需要对 clientSecret 进行验证,另外,AuthorizeEndpoint 只是生成 authorization_code,并没有生成 access_token,生成操作在 OpenAuthorizationCodeProvider 中的 Receive 方法。

OpenAuthorizationCodeProvider 示例代码:

public class OpenAuthorizationCodeProvider : AuthenticationTokenProvider
{
    private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);

    /// <summary>
    /// 生成 authorization_code
    /// </summary>
    public override void Create(AuthenticationTokenCreateContext context)
    {
        context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
        _authenticationCodes[context.Token] = context.SerializeTicket();
    }

    /// <summary>
    /// 由 authorization_code 解析成 access_token
    /// </summary>
    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        string value;
        if (_authenticationCodes.TryRemove(context.Token, out value))
        {
            context.DeserializeTicket(value);
        }
    }
}

上面 Create 方法是 await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext); 的重载方法。

OpenRefreshTokenProvider 示例代码:

public class OpenRefreshTokenProvider : AuthenticationTokenProvider
{
    private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>();

    /// <summary>
    /// 生成 refresh_token
    /// </summary>
    public override void Create(AuthenticationTokenCreateContext context)
    {
        context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60);

        context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
        _refreshTokens[context.Token] = context.SerializeTicket();
    }

    /// <summary>
    /// 由 refresh_token 解析成 access_token
    /// </summary>
    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        string value;
        if (_refreshTokens.TryRemove(context.Token, out value))
        {
            context.DeserializeTicket(value);
        }
    }
}

refresh_token 的作用就是,在 access_token 过期的时候,不需要再通过一些凭证申请 access_token,而是直接通过 refresh_token 就可以重新申请 access_token。

另外,需要一个 api 来接受 authorization_code(来自 redirect_uri 的回调跳转),实现代码如下:

public class CodesController : ApiController
{
    [HttpGet]
    [Route("api/authorization_code")]
    public HttpResponseMessage Get(string code)
    {
        return new HttpResponseMessage()
        {
            Content = new StringContent(code, Encoding.UTF8, "text/plain")
        };
    }
}

基本上面代码已经实现了,单元测试代码如下:

public class OAuthClientTest
{
    private const string HOST_ADDRESS = "http://localhost:8001";
    private IDisposable _webApp;
    private static HttpClient _httpClient;

    public OAuthClientTest()
    {
        _webApp = WebApp.Start<Startup>(HOST_ADDRESS);
        Console.WriteLine("Web API started!");
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri(HOST_ADDRESS);
        Console.WriteLine("HttpClient started!");
    }

    private static async Task<TokenResponse> GetToken(string grantType, string refreshToken = null, string userName = null, string password = null, string authorizationCode = null)
    {
        var clientId = "xishuai";
        var clientSecret = "123";
        var parameters = new Dictionary<string, string>();
        parameters.Add("grant_type", grantType);

        if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
        {
            parameters.Add("username", userName);
            parameters.Add("password", password);
        }
        if (!string.IsNullOrEmpty(authorizationCode))
        {
            parameters.Add("code", authorizationCode);
            parameters.Add("redirect_uri", "http://localhost:8001/api/authorization_code"); //和获取 authorization_code 的 redirect_uri 必须一致,不然会报错
        }
        if (!string.IsNullOrEmpty(refreshToken))
        {
            parameters.Add("refresh_token", refreshToken);
        }

        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Basic",
            Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));

        var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
        var responseValue = await response.Content.ReadAsStringAsync();
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine(response.StatusCode);
            Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
            return null;
        }
        return await response.Content.ReadAsAsync<TokenResponse>();
    }

    private static async Task<string> GetAuthorizationCode()
    {
        var clientId = "xishuai";

        var response = await _httpClient.GetAsync($"/authorize?grant_type=authorization_code&response_type=code&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/authorization_code")}");
        var authorizationCode = await response.Content.ReadAsStringAsync();
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine(response.StatusCode);
            Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
            return null;
        }
        return authorizationCode;
    }

    [Fact]
    public async Task OAuth_AuthorizationCode_Test()
    {
        var authorizationCode = GetAuthorizationCode().Result; //获取 authorization_code
        var tokenResponse = GetToken("authorization_code", null, null, null, authorizationCode).Result; //根据 authorization_code 获取 access_token
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

        var response = await _httpClient.GetAsync($"/api/values");
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine(response.StatusCode);
            Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
        }
        Console.WriteLine(await response.Content.ReadAsStringAsync());
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        Thread.Sleep(10000);

        var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result; //根据 refresh_token 获取 access_token
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
        var responseTwo = await _httpClient.GetAsync($"/api/values");
        Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
    }
}

Startup 配置的 access_token 过期时间是 10s,线程休眠 10s,是为了测试 refresh_token。

上面单元测试代码,执行成功,当然也可以用 Postman 模拟请求测试。

3. 简化模式(implicit grant type)

简单解释:授权码模式的简化版,省略 authorization_code,并且 access_token 以 URL 参数返回(比如 #token=xxxx)。

请求授权服务(只有一次),需要的参数:

  • response_type:必选,授权类型,值固定为 "token"。
  • client_id:必选,客户端 ID。
  • redirect_uri:必选,重定向 URI,URL 中会包含 access_token。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。
  • state:可选,客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值,比如微博授权服务值为 weibo。

需要注意的是,简化模式请求参数并不需要 grant_type,并且可以用 http get 直接请求。

Startup 代码:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        var OAuthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            AuthenticationMode = AuthenticationMode.Active,
            TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
            AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
            AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间

            Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
            RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
        };
        app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
    }
}

OpenRefreshTokenProvider、OpenAuthorizationServerProvider 的代码就不贴了,和上面授权码模式一样,只不过在 OpenAuthorizationServerProvider 的 AuthorizeEndpoint 方法中有 IsImplicitGrantType 判断,示例代码:

var identity = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(identity);
context.RequestCompleted();

这段代码执行会直接回调 redirect_uri,并附上 access_token,接受示例代码:

[HttpGet]
[Route("api/access_token")]
public HttpResponseMessage GetToken()
{
    var url = Request.RequestUri;
    return new HttpResponseMessage()
    {
        Content = new StringContent("", Encoding.UTF8, "text/plain")
    };
}

单元测试代码:

[Fact]
public async Task OAuth_Implicit_Test()
{
    var clientId = "xishuai";

    var tokenResponse = await _httpClient.GetAsync($"/authorize?response_type=token&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/access_token")}");
    //redirect_uri: http://localhost:8001/api/access_token#access_token=AQAAANCMnd8BFdERjHoAwE_Cl-sBAAAAfoPB4HZ0PUe-X6h0UUs2q42&token_type=bearer&expires_in=10
    var accessToken = "";//get form redirect_uri
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    var response = await _httpClient.GetAsync($"/api/values");
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
    }
    Console.WriteLine(await response.Content.ReadAsStringAsync());
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

回调 redirect_uri 中的 access_token 参数值,因为在 URL 的 # 后,后端不好获取到,所以这里的单元测试只是示例,并不能执行成功,建议使用 Poastman 进行测试。

4. 密码模式(resource owner password credentials)

简单解释:在一开始叙述的 OAuth 授权流程的时候,其实就是密码模式,落网发起授权请求,用户在微博的授权页面填写账号和密码,验证成功则返回 access_token,所以,在此过程中,用户填写的账号和密码,和落网没有半毛钱关系,不会存在账户信息被第三方窃取问题。

请求授权服务(只有一次),需要的参数:

  • grant_type:必选,授权模式,值固定为 "password"。
  • username:必选,用户名。
  • password:必选,用户密码。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。

Startup 代码:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        var OAuthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            AuthenticationMode = AuthenticationMode.Active,
            TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
            AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
            AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间

            Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
            RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
        };
        app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
    }
}

OpenAuthorizationServerProvider 示例代码:

public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    /// <summary>
    /// 验证 client 信息
    /// </summary>
    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        string clientId;
        string clientSecret;
        if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
        {
            context.TryGetFormCredentials(out clientId, out clientSecret);
        }

        if (clientId != "xishuai")
        {
            context.SetError("invalid_client", "client is not valid");
            return;
        }
        context.Validated();
    }

    /// <summary>
    /// 生成 access_token(resource owner password credentials 授权方式)
    /// </summary>
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (string.IsNullOrEmpty(context.UserName))
        {
            context.SetError("invalid_username", "username is not valid");
            return;
        }
        if (string.IsNullOrEmpty(context.Password))
        {
            context.SetError("invalid_password", "password is not valid");
            return;
        }

        if (context.UserName != "xishuai" || context.Password != "123")
        {
            context.SetError("invalid_identity", "username or password is not valid");
            return;
        }

        var OAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
        OAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        context.Validated(OAuthIdentity);
    }
}

GrantResourceOwnerCredentials 内部可以调用外部服务,以进行对用户账户信息的验证。

单元测试代码:

[Fact]
public async Task OAuth_Password_Test()
{
    var tokenResponse = GetToken("password", null, "xishuai", "123").Result; //获取 access_token
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

    var response = await _httpClient.GetAsync($"/api/values");
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
    }
    Console.WriteLine(await response.Content.ReadAsStringAsync());
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    Thread.Sleep(10000);

    var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result;
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
    var responseTwo = await _httpClient.GetAsync($"/api/values");
    Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}

5. 客户端模式(Client Credentials Grant)

简单解释:顾名思义,客户端模式就是客户端直接向授权服务发起请求,和用户没什么关系,也就是说落网直接向微博提交授权请求,此类的请求不包含用户信息,一般用作应用程序直接的交互等。

请求授权服务(只有一次),需要的参数:

  • grant_type:必选,授权模式,值固定为 "client_credentials"。
  • client_id:必选,客户端 ID。
  • client_secret:必选,客户端密码。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。

Startup 代码:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        var OAuthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            AuthenticationMode = AuthenticationMode.Active,
            TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
            AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
            AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间

            Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
            RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
        };
        app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
    }
}

OpenAuthorizationServerProvider 示例代码:

public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    /// <summary>
    /// 验证 client 信息
    /// </summary>
    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        string clientId;
        string clientSecret;
        if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
        {
            context.TryGetFormCredentials(out clientId, out clientSecret);
        }

        if (clientId != "xishuai" || clientSecret != "123")
        {
            context.SetError("invalid_client", "client or clientSecret is not valid");
            return;
        }
        context.Validated();
    }

    /// <summary>
    /// 生成 access_token(client credentials 授权方式)
    /// </summary>
    public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
    {
        var identity = new ClaimsIdentity(new GenericIdentity(
            context.ClientId, OAuthDefaults.AuthenticationType),
            context.Scope.Select(x => new Claim("urn:oauth:scope", x)));

        context.Validated(identity);
    }
}

和其他授权模式不同,客户端授权模式需要对 client_secret 进行验证(ValidateClientAuthentication)。

单元测试代码:

[Fact]
public async Task OAuth_ClientCredentials_Test()
{
    var tokenResponse = GetToken("client_credentials").Result; //获取 access_token
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

    var response = await _httpClient.GetAsync($"/api/values");
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
    }
    Console.WriteLine(await response.Content.ReadAsStringAsync());
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    Thread.Sleep(10000);

    var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result;
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
    var responseTwo = await _httpClient.GetAsync($"/api/values");
    Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}

除了上面四种授权模式之外,还有一种就是更新令牌(refresh token),单元测试代码中已经体现了,需要额外的两个参数:

  • grant_type:必选,授权模式,值固定为 "refresh_token"。
  • refresh_token:必选,授权返回的 refresh_token。

最后,总结下四种授权模式的应用场景:

  • 授权码模式(authorization code):引入 authorization_code,可以增加系统的安全性,和客户端应用场景差不多,但一般用于 Server 端。
  • 简化模式(implicit):无需 Server 端的介入,前端可以直接完成,一般用于前端操作。
  • 密码模式(resource owner password credentials):和用户账户相关,一般用于第三方登录。
  • 客户端模式(client credentials):和用户无关,一般用于应用程序和 api 之间的交互场景,比如落网开放出 api,供第三方开发者进行调用数据等。

开源地址:https://github.com/yuezhongxin/OAuth2.Demo

参考资料:


---------------------
作者:田园里的蟋蟀
来源:CNBLOGS
原文:https://www.cnblogs.com/xishuai/p/aspnet-webapi-owin-oauth2.html
版权声明:本文为作者原创文章,转载请附上博文链接!

Guess you like

Origin www.cnblogs.com/admans/p/11271601.html