From 744165183038543b4496bd2c5e929c1b1b6debea Mon Sep 17 00:00:00 2001 From: Luis Date: Wed, 27 Jan 2016 17:53:28 -0600 Subject: [PATCH] Create RefreshToken and Client Table Create class SimpleAuthorizationServerProvider and SimpleAuthorizationServerProvider Add Oauthentication Refresh Token in Api --- Api.Rest.Secure/Api.Rest.Secure.csproj | 21 ++- Api.Rest.Secure/AuthContext.cs | 5 + Api.Rest.Secure/AuthRepository.cs | 60 ++++++++- .../201601262034249_InitialCreate.Designer.cs | 29 ++++ .../201601262034249_InitialCreate.cs | 99 ++++++++++++++ .../201601262034249_InitialCreate.resx | 126 ++++++++++++++++++ ...Client_And_RefreshToken_Tables.Designer.cs | 29 ++++ ...6354_Add_Client_And_RefreshToken_Tables.cs | 45 +++++++ ...54_Add_Client_And_RefreshToken_Tables.resx | 126 ++++++++++++++++++ Api.Rest.Secure/Migrations/Configuration.cs | 32 +++++ Api.Rest.Secure/Models/Entities/Client.cs | 20 +++ .../Models/Entities/RefreshToken.cs | 21 +++ Api.Rest.Secure/Models/UserModel.cs | 6 + .../SimpleAuthorizationServerProvider.cs | 110 ++++++++++++++- .../Providers/SimpleRefreshTokenProvider.cs | 67 ++++++++++ Api.Rest.Secure/StartUp.cs | 1 + 16 files changed, 787 insertions(+), 10 deletions(-) create mode 100644 Api.Rest.Secure/Migrations/201601262034249_InitialCreate.Designer.cs create mode 100644 Api.Rest.Secure/Migrations/201601262034249_InitialCreate.cs create mode 100644 Api.Rest.Secure/Migrations/201601262034249_InitialCreate.resx create mode 100644 Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.Designer.cs create mode 100644 Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.cs create mode 100644 Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.resx create mode 100644 Api.Rest.Secure/Migrations/Configuration.cs create mode 100644 Api.Rest.Secure/Models/Entities/Client.cs create mode 100644 Api.Rest.Secure/Models/Entities/RefreshToken.cs create mode 100644 Api.Rest.Secure/Providers/SimpleRefreshTokenProvider.cs diff --git a/Api.Rest.Secure/Api.Rest.Secure.csproj b/Api.Rest.Secure/Api.Rest.Secure.csproj index 447ab5b..46221e9 100644 --- a/Api.Rest.Secure/Api.Rest.Secure.csproj +++ b/Api.Rest.Secure/Api.Rest.Secure.csproj @@ -148,12 +148,31 @@ + + + 201601262034249_InitialCreate.cs + + + + 201601272316354_Add_Client_And_RefreshToken_Tables.cs + + + + + - + + + 201601262034249_InitialCreate.cs + + + 201601272316354_Add_Client_And_RefreshToken_Tables.cs + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/Api.Rest.Secure/AuthContext.cs b/Api.Rest.Secure/AuthContext.cs index f59f1cb..8a32ce8 100644 --- a/Api.Rest.Secure/AuthContext.cs +++ b/Api.Rest.Secure/AuthContext.cs @@ -1,8 +1,10 @@ using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; +using System.Data.Entity; using System.Linq; using System.Web; +using Api.Rest.Secure.Models.Entities; namespace Api.Rest.Secure { @@ -13,5 +15,8 @@ public AuthContext() { } + + public DbSet Clients { get; set; } + public DbSet RefreshTokens { get; set; } } } \ No newline at end of file diff --git a/Api.Rest.Secure/AuthRepository.cs b/Api.Rest.Secure/AuthRepository.cs index add2854..992a4bf 100644 --- a/Api.Rest.Secure/AuthRepository.cs +++ b/Api.Rest.Secure/AuthRepository.cs @@ -6,14 +6,15 @@ using System.Linq; using System.Threading.Tasks; using System.Web; +using Api.Rest.Secure.Models.Entities; namespace Api.Rest.Secure { public class AuthRepository : IDisposable { - private AuthContext _ctx; + private readonly AuthContext _ctx; - private UserManager _userManager; + private readonly UserManager _userManager; public AuthRepository() { @@ -46,5 +47,60 @@ public void Dispose() _userManager.Dispose(); } + + + public Client FindClient(string clientId) + { + var client = _ctx.Clients.Find(clientId); + + return client; + } + + public async Task AddRefreshToken(RefreshToken token) + { + + var existingToken = _ctx.RefreshTokens.SingleOrDefault(r => r.Subject == token.Subject && r.ClientId == token.ClientId); + + if (existingToken != null) + { + await RemoveRefreshToken(existingToken); + } + + _ctx.RefreshTokens.Add(token); + + return await _ctx.SaveChangesAsync() > 0; + } + + public async Task RemoveRefreshToken(string refreshTokenId) + { + var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId); + + if (refreshToken != null) + { + _ctx.RefreshTokens.Remove(refreshToken); + return await _ctx.SaveChangesAsync() > 0; + } + + return false; + } + + public async Task RemoveRefreshToken(RefreshToken refreshToken) + { + _ctx.RefreshTokens.Remove(refreshToken); + return await _ctx.SaveChangesAsync() > 0; + } + + public async Task FindRefreshToken(string refreshTokenId) + { + var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId); + + return refreshToken; + } + + public List GetAllRefreshTokens() + { + return _ctx.RefreshTokens.ToList(); + } + } } \ No newline at end of file diff --git a/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.Designer.cs b/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.Designer.cs new file mode 100644 index 0000000..440682c --- /dev/null +++ b/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.Designer.cs @@ -0,0 +1,29 @@ +// +namespace Api.Rest.Secure.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class InitialCreate : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(InitialCreate)); + + string IMigrationMetadata.Id + { + get { return "201601262034249_InitialCreate"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.cs b/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.cs new file mode 100644 index 0000000..af0cdc9 --- /dev/null +++ b/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.cs @@ -0,0 +1,99 @@ +namespace Api.Rest.Secure.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class InitialCreate : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.AspNetRoles", + c => new + { + Id = c.String(nullable: false, maxLength: 128), + Name = c.String(nullable: false, maxLength: 256), + }) + .PrimaryKey(t => t.Id) + .Index(t => t.Name, unique: true, name: "RoleNameIndex"); + + CreateTable( + "dbo.AspNetUserRoles", + c => new + { + UserId = c.String(nullable: false, maxLength: 128), + RoleId = c.String(nullable: false, maxLength: 128), + }) + .PrimaryKey(t => new { t.UserId, t.RoleId }) + .ForeignKey("dbo.AspNetRoles", t => t.RoleId, cascadeDelete: true) + .ForeignKey("dbo.AspNetUsers", t => t.UserId, cascadeDelete: true) + .Index(t => t.UserId) + .Index(t => t.RoleId); + + CreateTable( + "dbo.AspNetUsers", + c => new + { + Id = c.String(nullable: false, maxLength: 128), + Email = c.String(maxLength: 256), + EmailConfirmed = c.Boolean(nullable: false), + PasswordHash = c.String(), + SecurityStamp = c.String(), + PhoneNumber = c.String(), + PhoneNumberConfirmed = c.Boolean(nullable: false), + TwoFactorEnabled = c.Boolean(nullable: false), + LockoutEndDateUtc = c.DateTime(), + LockoutEnabled = c.Boolean(nullable: false), + AccessFailedCount = c.Int(nullable: false), + UserName = c.String(nullable: false, maxLength: 256), + }) + .PrimaryKey(t => t.Id) + .Index(t => t.UserName, unique: true, name: "UserNameIndex"); + + CreateTable( + "dbo.AspNetUserClaims", + c => new + { + Id = c.Int(nullable: false, identity: true), + UserId = c.String(nullable: false, maxLength: 128), + ClaimType = c.String(), + ClaimValue = c.String(), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.AspNetUsers", t => t.UserId, cascadeDelete: true) + .Index(t => t.UserId); + + CreateTable( + "dbo.AspNetUserLogins", + c => new + { + LoginProvider = c.String(nullable: false, maxLength: 128), + ProviderKey = c.String(nullable: false, maxLength: 128), + UserId = c.String(nullable: false, maxLength: 128), + }) + .PrimaryKey(t => new { t.LoginProvider, t.ProviderKey, t.UserId }) + .ForeignKey("dbo.AspNetUsers", t => t.UserId, cascadeDelete: true) + .Index(t => t.UserId); + + } + + public override void Down() + { + DropForeignKey("dbo.AspNetUserRoles", "UserId", "dbo.AspNetUsers"); + DropForeignKey("dbo.AspNetUserLogins", "UserId", "dbo.AspNetUsers"); + DropForeignKey("dbo.AspNetUserClaims", "UserId", "dbo.AspNetUsers"); + DropForeignKey("dbo.AspNetUserRoles", "RoleId", "dbo.AspNetRoles"); + DropIndex("dbo.AspNetUserLogins", new[] { "UserId" }); + DropIndex("dbo.AspNetUserClaims", new[] { "UserId" }); + DropIndex("dbo.AspNetUsers", "UserNameIndex"); + DropIndex("dbo.AspNetUserRoles", new[] { "RoleId" }); + DropIndex("dbo.AspNetUserRoles", new[] { "UserId" }); + DropIndex("dbo.AspNetRoles", "RoleNameIndex"); + DropTable("dbo.AspNetUserLogins"); + DropTable("dbo.AspNetUserClaims"); + DropTable("dbo.AspNetUsers"); + DropTable("dbo.AspNetUserRoles"); + DropTable("dbo.AspNetRoles"); + } + } +} diff --git a/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.resx b/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.resx new file mode 100644 index 0000000..c214c8c --- /dev/null +++ b/Api.Rest.Secure/Migrations/201601262034249_InitialCreate.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.Designer.cs b/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.Designer.cs new file mode 100644 index 0000000..c266a49 --- /dev/null +++ b/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.Designer.cs @@ -0,0 +1,29 @@ +// +namespace Api.Rest.Secure.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class Add_Client_And_RefreshToken_Tables : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Add_Client_And_RefreshToken_Tables)); + + string IMigrationMetadata.Id + { + get { return "201601272316354_Add_Client_And_RefreshToken_Tables"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.cs b/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.cs new file mode 100644 index 0000000..875d985 --- /dev/null +++ b/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.cs @@ -0,0 +1,45 @@ +namespace Api.Rest.Secure.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class Add_Client_And_RefreshToken_Tables : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.Clients", + c => new + { + Id = c.String(nullable: false, maxLength: 128), + Secret = c.String(nullable: false), + Name = c.String(nullable: false, maxLength: 100), + ApplicationType = c.Int(nullable: false), + Active = c.Boolean(nullable: false), + RefreshTokenLifeTime = c.Int(nullable: false), + AllowedOrigin = c.String(maxLength: 100), + }) + .PrimaryKey(t => t.Id); + + CreateTable( + "dbo.RefreshTokens", + c => new + { + Id = c.String(nullable: false, maxLength: 128), + Subject = c.String(nullable: false, maxLength: 50), + ClientId = c.String(nullable: false, maxLength: 50), + IssuedUtc = c.DateTime(nullable: false), + ExpiresUtc = c.DateTime(nullable: false), + ProtectedTicket = c.String(nullable: false), + }) + .PrimaryKey(t => t.Id); + + } + + public override void Down() + { + DropTable("dbo.RefreshTokens"); + DropTable("dbo.Clients"); + } + } +} diff --git a/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.resx b/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.resx new file mode 100644 index 0000000..c214c8c --- /dev/null +++ b/Api.Rest.Secure/Migrations/201601272316354_Add_Client_And_RefreshToken_Tables.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/Api.Rest.Secure/Migrations/Configuration.cs b/Api.Rest.Secure/Migrations/Configuration.cs new file mode 100644 index 0000000..c886fdd --- /dev/null +++ b/Api.Rest.Secure/Migrations/Configuration.cs @@ -0,0 +1,32 @@ +namespace Api.Rest.Secure.Migrations +{ + using System; + using System.Data.Entity; + using System.Data.Entity.Migrations; + using System.Linq; + + internal sealed class Configuration : DbMigrationsConfiguration + { + public Configuration() + { + AutomaticMigrationsEnabled = false; + ContextKey = "Api.Rest.Secure.AuthContext"; + } + + protected override void Seed(Api.Rest.Secure.AuthContext context) + { + // This method will be called after migrating to the latest version. + + // You can use the DbSet.AddOrUpdate() helper extension method + // to avoid creating duplicate seed data. E.g. + // + // context.People.AddOrUpdate( + // p => p.FullName, + // new Person { FullName = "Andrew Peters" }, + // new Person { FullName = "Brice Lambson" }, + // new Person { FullName = "Rowan Miller" } + // ); + // + } + } +} diff --git a/Api.Rest.Secure/Models/Entities/Client.cs b/Api.Rest.Secure/Models/Entities/Client.cs new file mode 100644 index 0000000..c4687ba --- /dev/null +++ b/Api.Rest.Secure/Models/Entities/Client.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Api.Rest.Secure.Models.Entities +{ + public class Client + { + [Key] + public string Id { get; set; } + [Required] + public string Secret { get; set; } + [Required] + [MaxLength(100)] + public string Name { get; set; } + public ApplicationTypes ApplicationType { get; set; } + public bool Active { get; set; } + public int RefreshTokenLifeTime { get; set; } + [MaxLength(100)] + public string AllowedOrigin { get; set; } + } +} diff --git a/Api.Rest.Secure/Models/Entities/RefreshToken.cs b/Api.Rest.Secure/Models/Entities/RefreshToken.cs new file mode 100644 index 0000000..a2d1c52 --- /dev/null +++ b/Api.Rest.Secure/Models/Entities/RefreshToken.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Api.Rest.Secure.Models.Entities +{ + public class RefreshToken + { + [Key] + public string Id { get; set; } + [Required] + [MaxLength(50)] + public string Subject { get; set; } + [Required] + [MaxLength(50)] + public string ClientId { get; set; } + public DateTime IssuedUtc { get; set; } + public DateTime ExpiresUtc { get; set; } + [Required] + public string ProtectedTicket { get; set; } + } +} \ No newline at end of file diff --git a/Api.Rest.Secure/Models/UserModel.cs b/Api.Rest.Secure/Models/UserModel.cs index 72068c0..5e39b5c 100644 --- a/Api.Rest.Secure/Models/UserModel.cs +++ b/Api.Rest.Secure/Models/UserModel.cs @@ -23,4 +23,10 @@ public class UserModel [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } + + public enum ApplicationTypes + { + JavaScript = 0, + NativeConfidential = 1 + }; } \ No newline at end of file diff --git a/Api.Rest.Secure/Providers/SimpleAuthorizationServerProvider.cs b/Api.Rest.Secure/Providers/SimpleAuthorizationServerProvider.cs index d7ce650..04a2e00 100644 --- a/Api.Rest.Secure/Providers/SimpleAuthorizationServerProvider.cs +++ b/Api.Rest.Secure/Providers/SimpleAuthorizationServerProvider.cs @@ -1,23 +1,83 @@ -using Microsoft.AspNet.Identity.EntityFramework; +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security.OAuth; using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; +using Api.Rest.Secure.Models.Entities; +using Microsoft.Owin.Security; namespace Api.Rest.Secure.Providers { public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider { - public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) - { - await Task.FromResult(context.Validated()); + public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) + { + string clientId; + string clientSecret; + Client client; + + 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(null); + } + + using (var repo = new AuthRepository()) + { + client = repo.FindClient(context.ClientId); + } + + if (client == null) + { + context.SetError("invalid_clientId", $"Client '{context.ClientId}' is not registered in the system."); + return Task.FromResult(null); + } + + if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential) + { + if (string.IsNullOrWhiteSpace(clientSecret)) + { + context.SetError("invalid_clientId", "Client secret should be sent."); + return Task.FromResult(null); + } + if (client.Secret != Helper.GetHash(clientSecret)) + { + context.SetError("invalid_clientId", "Client secret is invalid."); + return Task.FromResult(null); + } + } + + if (!client.Active) + { + context.SetError("invalid_clientId", "Client is inactive."); + return Task.FromResult(null); + } + + context.OwinContext.Set("as:clientAllowedOrigin", client.AllowedOrigin); + context.OwinContext.Set("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); + + context.Validated(); + return Task.FromResult(null); } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { + var allowedOrigin = context.OwinContext.Get("as:clientAllowedOrigin") ?? "*"; - //context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); + context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin }); - using (AuthRepository _repo = new AuthRepository()) + using (var _repo = new AuthRepository()) { IdentityUser user = await _repo.FindUser(context.UserName, context.Password); @@ -29,11 +89,47 @@ public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwner } var identity = new ClaimsIdentity(context.Options.AuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); identity.AddClaim(new Claim("sub", context.UserName)); identity.AddClaim(new Claim("role", "user")); - context.Validated(identity); + var props = new AuthenticationProperties(new Dictionary + { + { + "as:client_id", context.ClientId ?? string.Empty + }, + { + "userName", context.UserName + } + }); + + var ticket = new AuthenticationTicket(identity, props); + context.Validated(ticket); + + } + + public override Task TokenEndpoint(OAuthTokenEndpointContext context) + { + foreach (KeyValuePair property in context.Properties.Dictionary) + { + context.AdditionalResponseParameters.Add(property.Key, property.Value); + } + + return Task.FromResult(null); + } + } + + public class Helper + { + public static string GetHash(string input) + { + HashAlgorithm hashAlgorithm = new SHA256CryptoServiceProvider(); + + var byteValue = Encoding.UTF8.GetBytes(input); + + var byteHash = hashAlgorithm.ComputeHash(byteValue); + return Convert.ToBase64String(byteHash); } } } \ No newline at end of file diff --git a/Api.Rest.Secure/Providers/SimpleRefreshTokenProvider.cs b/Api.Rest.Secure/Providers/SimpleRefreshTokenProvider.cs new file mode 100644 index 0000000..512ab7b --- /dev/null +++ b/Api.Rest.Secure/Providers/SimpleRefreshTokenProvider.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using Api.Rest.Secure.Models.Entities; +using Microsoft.Owin.Security.Infrastructure; + +namespace Api.Rest.Secure.Providers +{ + public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider + { + public void Create(AuthenticationTokenCreateContext context) + { + throw new NotImplementedException(); + } + + public async Task CreateAsync(AuthenticationTokenCreateContext context) + { + var clientid = context.Ticket.Properties.Dictionary["as:client_id"]; + + if (string.IsNullOrEmpty(clientid)) + { + return; + } + + var refreshTokenId = Guid.NewGuid().ToString("n"); + + using (AuthRepository _repo = new AuthRepository()) + { + var refreshTokenLifeTime = context.OwinContext.Get("as:clientRefreshTokenLifeTime"); + + var token = new RefreshToken() + { + Id = Helper.GetHash(refreshTokenId), + ClientId = clientid, + Subject = context.Ticket.Identity.Name, + IssuedUtc = DateTime.UtcNow, + ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime)) + }; + + context.Ticket.Properties.IssuedUtc = token.IssuedUtc; + context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc; + + token.ProtectedTicket = context.SerializeTicket(); + + var result = await _repo.AddRefreshToken(token); + + if (result) + { + context.SetToken(refreshTokenId); + } + + } + } + + public void Receive(AuthenticationTokenReceiveContext context) + { + throw new NotImplementedException(); + } + + public Task ReceiveAsync(AuthenticationTokenReceiveContext context) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Api.Rest.Secure/StartUp.cs b/Api.Rest.Secure/StartUp.cs index 37d3b75..a5b398b 100644 --- a/Api.Rest.Secure/StartUp.cs +++ b/Api.Rest.Secure/StartUp.cs @@ -32,6 +32,7 @@ public void ConfigureOAuth(IAppBuilder app) TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), Provider = new SimpleAuthorizationServerProvider(), + RefreshTokenProvider = new SimpleRefreshTokenProvider() };