7
votes

I need to consume ASP.NET 5 secure Web API from a web client using local accounts. In the past there was a handler issuing bearer tokens in order to support OAuth, now bearer token issuance responsibility was removed from identity. Some people recommends to use identityServer3, which require clients registration, unlike identity2 approach. What is the simplest way to achieve authorization in ASP.NET 5 Web API? How can I avoid to pass client id and client secret in order to get an access token when using resource owner password flow? How to call api avoiding to pass scope?

1

1 Answers

0
votes

I have built a simple bearer token issuer from this but using Identity password hasher. You can see full code below:

public class TokenController : Controller
{
    private readonly IBearerTokenGenerator generator;
    private readonly IClientsManager clientsManager;
    private readonly IOptions<TokenAuthOptions> options;

    public TokenController(IBearerTokenGenerator generator,
        IClientsManager clientsManager,
        IOptions<TokenAuthOptions> options)
    {
        this.generator = generator;
        this.clientsManager = clientsManager;
        this.options = options;
    }

    [HttpPost, AllowAnonymous]
    public IActionResult Post(AuthenticationViewModel req)
    {
        return clientsManager
            .Find(req.client_id, req.client_secret)
            .Map(c => c.Client)
            .Map(c => (IActionResult)new ObjectResult(new {
                access_token = generator.Generate(c),
                expires_in = options.Value.ExpirationDelay.TotalSeconds,
                token_type = "Bearer"
            }))
            .ValueOrDefault(HttpUnauthorized);
    }
}

public class BearerTokenGenerator : IBearerTokenGenerator
{
    private readonly IOptions<TokenAuthOptions> tokenOptions;

    public BearerTokenGenerator(IOptions<TokenAuthOptions> tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }

    public string Generate(Client client)
    {
        var expires = Clock.UtcNow.Add(tokenOptions.Value.ExpirationDelay);
        var handler = new JwtSecurityTokenHandler();

        var identity = new ClaimsIdentity(new GenericIdentity(client.Identifier, "TokenAuth"), new Claim[] {
            new Claim("client_id", client.Identifier)
        });

        var securityToken = handler.CreateToken(
            issuer: tokenOptions.Value.Issuer,
            audience: tokenOptions.Value.Audience,
            signingCredentials: tokenOptions.Value.SigningCredentials,
            subject: identity,
            expires: expires);

        return handler.WriteToken(securityToken);
    }
}

public class ClientsManager : IClientsManager
{
    private readonly MembershipDataContext db;
    private readonly ISecretHasher hasher;

    public ClientsManager(MembershipDataContext db,
        ISecretHasher hasher)
    {
        this.db = db;
        this.hasher = hasher;
    }

    public void Create(string name, string identifier, string secret, Company company)
    {
        var client = new Client(name, identifier, company);
        db.Clients.Add(client);

        var hash = hasher.HashSecret(secret);
        var apiClient = new ApiClient(client, hash);

        db.ApiClients.Add(apiClient);
    }

    public Option<ApiClient> Find(string identifier, string secret)
    {
        return FindByIdentifier(identifier)
            .Where(c => hasher.Verify(c.SecretHash, secret));
    }

    public void ChangeSecret(string identifier, string secret)
    {
        var client = FindByIdentifier(identifier).ValueOrDefault(() => {
            throw new ArgumentException($"could not find any client with identifier { identifier }");
        });

        var hash = hasher.HashSecret(secret);
        client.ChangeSecret(hash);
    }

    public string GenerateRandomSecret()
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        var random = new Random();
        var generated = new string(Enumerable.Repeat(chars, 12).Select(s => s[random.Next(s.Length)]).ToArray());
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(generated));
    }

    private Option<ApiClient> FindByIdentifier(string identifier)
    {
        return db.ApiClients
            .Include(c => c.Client)
            .SingleOrDefault(c => c.Client.Identifier == identifier)
            .ToOptionByRef();
    }
}

public class SecretHasher : ISecretHasher
{
    private static Company fakeCompany = new Company("fake", "fake");
    private static Client fakeClient = new Client("fake", "fake", fakeCompany);

    private readonly IPasswordHasher<Client> hasher;

    public SecretHasher(IPasswordHasher<Client> hasher)
    {
        this.hasher = hasher;
    }

    public string HashSecret(string secret)
    {
        return hasher.HashPassword(fakeClient, secret);
    }

    public bool Verify(string hashed, string secret)
    {
        return hasher.VerifyHashedPassword(fakeClient, hashed, secret) == PasswordVerificationResult.Success;
    }
}

Now in Startup.cs

            services.Configure<TokenAuthOptions>(options =>
        {
            options.Audience = "API";
            options.Issuer = "Web-App";
            options.SigningCredentials = new SigningCredentials(GetSigninKey(), SecurityAlgorithms.RsaSha256Signature);
            options.ExpirationDelay = TimeSpan.FromDays(365);
        });

            services.AddAuthorization(auth =>
        {
            auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
                .RequireAuthenticatedUser()
                .Build());
        }

        app.UseJwtBearerAuthentication(options =>
        {
            options.TokenValidationParameters.IssuerSigningKey = GetSigninKey();
            options.TokenValidationParameters.ValidAudience = "API";
            options.TokenValidationParameters.ValidIssuer = "Web-App";
            options.TokenValidationParameters.ValidateSignature = true;
            options.TokenValidationParameters.ValidateLifetime = true;
            options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(0);
        });