Search code examples
c#sql-serveroauth-2.0asp.net-web-api2membership-provider

How to implement REST API with OAuth 2.0 for multiple client access


I have a requirement like below to implement REST API using OAuth 2.0 and Web Api.

REST API should allow - to create, update, view and delete orders - to create, update, view and delete inventories

API should be able to used by any type of external client such as web application, mobile application, windows/web services, etc.

Roles allowed for external clients : Order Management , Inventory Management User data (roles, permissions) of external clients will not be managed by our system.

Note: There can be another two roles like Internal , External. Because delete functions can't be allowed for external users.

Order and Inventory data will be managed in a SQL Server DB which is already used by current windows/desktop applications. Orders, inventories comes via new API should save in same database.

Questions:

  1. Which grant type I can use?
  2. How should I mange external client's data (allowed roles, client id, tokens) ? Do I need to use separate membership database for this? Can I used my existing database with new tables for this?

Solution

  • You can use Microsoft.Owin.Security.OAuth provider. Please have a look on following sample.

    Create new Owin Startup file and change the Configuration method as following

    public void Configuration(IAppBuilder app)
    {
        var oauthProvider = new OAuthAuthorizationServerProvider
        {
            OnGrantClientCredentials = async context =>
            {
    
                var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
                // based on clientId get roles and add claims
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Developer"));
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Developer2"));
                context.Validated(claimsIdentity);
            },
            OnValidateClientAuthentication = async context =>
            {
                string clientId;
                string clientSecret;
                // use context.TryGetBasicCredentials in case of passing values in header
                if (context.TryGetFormCredentials(out clientId, out clientSecret))
                {
                    if (clientId == "clientId" && clientSecret == "secretKey")
                    {
                        context.Validated(clientId);
                    }
                }
            }
        };
        var oauthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/accesstoken"),
            Provider = oauthProvider,
            AuthorizationCodeExpireTimeSpan = TimeSpan.FromMinutes(1),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(3),
            SystemClock = new SystemClock()
        };
        app.UseOAuthAuthorizationServer(oauthOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
    
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();
        app.UseWebApi(config);
    }
    

    And authorize your API like this

    [Authorize(Roles = "Developer")]
    // GET: api/Tests
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
    

    you can consume it like following,

    string baseAddress = "http://localhost/";
    var client = new HttpClient();
    
    // you can pass the values in Authorization header or as form data
    //var authorizationHeader = Convert.ToBase64String(Encoding.UTF8.GetBytes("clientId:secretKey"));
    //client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorizationHeader);
    
    var form = new Dictionary<string, string>
        {
            {"grant_type", "client_credentials"},
            {"client_id", "clientId"},
            {"client_secret", "secretKey"},
        };
    
    var tokenResponse = client.PostAsync(baseAddress + "accesstoken", new FormUrlEncodedContent(form)).Result;
    var token = tokenResponse.Content.ReadAsAsync<Token>(new[] { new JsonMediaTypeFormatter() }).Result;
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
    var authorizedResponse = client.GetAsync(baseAddress + "/api/Tests").Result;
    

    Token.cs

    internal class Token
    {
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    
        [JsonProperty("token_type")]
        public string TokenType { get; set; }
    
        [JsonProperty("expires_in")]
        public int ExpiresIn { get; set; }
    
        [JsonProperty("refresh_token")]
        public string RefreshToken { get; set; }
    }
    

    answers to your questions

    1. You can use client_credentials
    2. Maintain roles in your own database for each client and inside OnGrantClientCredentials just get roles by client id and assign as claims.