1
0
mirror of https://git.teknik.io/Teknikode/Teknik.git synced 2023-08-02 14:16:22 +02:00

Added Auth Tokens for direct API access

This commit is contained in:
Uncled1023 2021-12-06 00:13:12 -08:00
parent 37fc3ca560
commit 2233d6c3be
35 changed files with 1351 additions and 193 deletions

View File

@ -0,0 +1,4 @@
{
"version": "5.1.0",
"hash": "37fc3ca56005b7eca9143ae4c74f4163e9417856"
}

View File

@ -0,0 +1,4 @@
{
"version": "{{git_ver}}",
"hash": "{{git_hash}}"
}

View File

@ -1,11 +1,40 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq;
using Teknik.IdentityServer.Models; using Teknik.IdentityServer.Models;
using Teknik.Utilities.Attributes;
namespace Teknik.IdentityServer namespace Teknik.IdentityServer
{ {
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{ {
public DbSet<AuthToken> AuthTokens { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// User
modelBuilder.Entity<ApplicationUser>().HasMany(u => u.AuthTokens).WithOne(t => t.ApplicationUser).HasForeignKey(t => t.ApplicationUserId);
// Auth Tokens
modelBuilder.Entity<AuthToken>().ToTable("AuthTokens");
// Custom Attributes
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
{
var attributes = property?.PropertyInfo?.GetCustomAttributes(typeof(CaseSensitiveAttribute), false);
if (attributes != null && attributes.Any())
{
property.SetAnnotation("CaseSensitive", true);
}
}
}
}
} }
} }

View File

@ -139,6 +139,36 @@ namespace Teknik.IdentityServer.Controllers
return new JsonResult(new { success = false, message = "User does not exist." }); return new JsonResult(new { success = false, message = "User does not exist." });
} }
[HttpGet]
public IActionResult GetUserInfoByAuthToken(string authToken)
{
if (string.IsNullOrEmpty(authToken))
return new JsonResult(new { success = false, message = "Auth Token Required" });
var foundUser = GetCachedUserByAuthToken(authToken);
if (foundUser != null)
{
var userJson = foundUser.ToJson();
return new JsonResult(new { success = true, data = userJson });
}
return new JsonResult(new { success = false, message = "User does not exist." });
}
[HttpGet]
public async Task<IActionResult> GetUserClaims(string username, [FromServices] IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
{
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await GetCachedUser(username);
if (foundUser != null)
{
var principal = await claimsFactory.CreateAsync(foundUser);
return new JsonResult(new { success = true, data = principal.Claims.ToList() });
}
return new JsonResult(new { success = false, message = "User does not exist." });
}
[HttpPost] [HttpPost]
public async Task<IActionResult> CheckPassword(CheckPasswordModel model) public async Task<IActionResult> CheckPassword(CheckPasswordModel model)
{ {
@ -672,6 +702,109 @@ namespace Teknik.IdentityServer.Controllers
return new JsonResult(new { success = false, message = "Client does not exist." }); return new JsonResult(new { success = false, message = "Client does not exist." });
} }
[HttpGet]
public IActionResult GetAuthToken(string authTokenId, [FromServices] ApplicationDbContext dbContext)
{
var authToken = dbContext.AuthTokens.FirstOrDefault(t => t.AuthTokenId == Guid.Parse(authTokenId));
if (authToken != null)
{
return new JsonResult(new { success = true, data = new { authTokenId = authToken.AuthTokenId, name = authToken.Name, token = authToken.Token } });
}
return new JsonResult(new { success = false, message = "Auth Token does not exist." });
}
[HttpGet]
public async Task<IActionResult> GetAuthTokens(string username)
{
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await GetCachedUser(username);
if (foundUser != null)
{
var authTokens = foundUser.AuthTokens.Select(t => new { authTokenId = t.AuthTokenId, name = t.Name, token = t.Token }).ToList();
return new JsonResult(new { success = true, data = authTokens });
}
return new JsonResult(new { success = false, message = "User does not exist" });
}
[HttpPost]
public IActionResult CreateAuthToken(CreateAuthTokenModel model, [FromServices] ApplicationDbContext dbContext)
{
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = dbContext.Users.FirstOrDefault(u => u.UserName == model.Username);
if (foundUser != null)
{
// Generate a unique token
var token = StringHelper.RandomString(40, "abcdefghjkmnpqrstuvwxyz1234567890");
var authToken = new AuthToken()
{
AuthTokenId = Guid.NewGuid(),
ApplicationUser = foundUser,
Name = model.Name,
Token = token
};
dbContext.AuthTokens.Add(authToken);
dbContext.SaveChanges();
// Clear the user cache
RemoveCachedUser(model.Username);
return new JsonResult(new { success = true, data = new { authTokenId = authToken.AuthTokenId, token = token } });
}
return new JsonResult(new { success = false, message = "User does not exist" });
}
[HttpPost]
public IActionResult EditAuthToken(EditAuthTokenModel model, [FromServices] ApplicationDbContext applicationDb)
{
var foundAuthToken = applicationDb.AuthTokens.FirstOrDefault(t => t.AuthTokenId == Guid.Parse(model.AuthTokenId));
if (foundAuthToken != null)
{
foundAuthToken.Name = model.Name;
applicationDb.Entry(foundAuthToken).State = EntityState.Modified;
applicationDb.SaveChanges();
// Clear the user cache
RemoveCachedUser(foundAuthToken.ApplicationUser.UserName);
// Clear the user cache by token
RemoveCachedUser(foundAuthToken.Token);
return new JsonResult(new { success = true });
}
return new JsonResult(new { success = false, message = "Auth Token does not exist." });
}
[HttpPost]
public IActionResult DeleteAuthToken(DeleteAuthTokenModel model, [FromServices] ApplicationDbContext applicationDb)
{
var foundAuthToken = applicationDb.AuthTokens.FirstOrDefault(t => t.AuthTokenId == Guid.Parse(model.AuthTokenId));
if (foundAuthToken != null)
{
var username = foundAuthToken.ApplicationUser.UserName;
var token = foundAuthToken.Token;
applicationDb.AuthTokens.Remove(foundAuthToken);
applicationDb.SaveChanges();
// Clear the user cache
RemoveCachedUser(username);
// Clear the user cache by token
RemoveCachedUser(token);
return new JsonResult(new { success = true });
}
return new JsonResult(new { success = false, message = "Auth Token does not exist." });
}
private string FormatKey(string unformattedKey) private string FormatKey(string unformattedKey)
{ {
var result = new StringBuilder(); var result = new StringBuilder();
@ -709,6 +842,26 @@ namespace Teknik.IdentityServer.Controllers
return foundUser; return foundUser;
} }
private ApplicationUser GetCachedUserByAuthToken(string token)
{
if (string.IsNullOrEmpty(token))
throw new ArgumentNullException("token");
// Check the cache
string cacheKey = GetKey<ApplicationUser>(token);
ApplicationUser foundUser;
if (!_cache.TryGetValue(cacheKey, out foundUser))
{
foundUser = _userManager.Users.FirstOrDefault(u => u.AuthTokens.FirstOrDefault(t => t.Token == token) != null);
if (foundUser != null)
{
_cache.AddToCache(cacheKey, foundUser, new TimeSpan(1, 0, 0));
}
}
return foundUser;
}
private void RemoveCachedUser(string username) private void RemoveCachedUser(string username)
{ {
if (string.IsNullOrEmpty(username)) if (string.IsNullOrEmpty(username))

View File

@ -0,0 +1,328 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Teknik.IdentityServer;
namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20211206044736_AuthToken")]
partial class AuthToken
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("ProductVersion", "5.0.7")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<int>("AccountStatus")
.HasColumnType("int");
b.Property<int>("AccountType")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<DateTime>("LastEdit")
.HasColumnType("datetime2");
b.Property<DateTime>("LastSeen")
.HasColumnType("datetime2");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PGPPublicKey")
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Teknik.IdentityServer.Models.AuthToken", b =>
{
b.Property<Guid>("AuthTokenId")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<string>("Token")
.HasColumnType("nvarchar(max)")
.HasAnnotation("CaseSensitive", true);
b.HasKey("AuthTokenId");
b.HasIndex("ApplicationUserId");
b.ToTable("AuthTokens");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Teknik.IdentityServer.Models.AuthToken", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("AuthTokens")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Navigation("AuthTokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
public partial class AuthToken : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AuthTokens",
columns: table => new
{
AuthTokenId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
ApplicationUserId = table.Column<string>(type: "nvarchar(450)", nullable: true),
Token = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AuthTokens", x => x.AuthTokenId);
table.ForeignKey(
name: "FK_AuthTokens_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_AuthTokens_ApplicationUserId",
table: "AuthTokens",
column: "ApplicationUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AuthTokens");
}
}
}

View File

@ -16,13 +16,12 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024") .HasAnnotation("ProductVersion", "5.0.7")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp") b.Property<string>("ConcurrencyStamp")
@ -154,7 +153,6 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b => modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount") b.Property<int>("AccessFailedCount")
@ -235,12 +233,36 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
modelBuilder.Entity("Teknik.IdentityServer.Models.AuthToken", b =>
{
b.Property<Guid>("AuthTokenId")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<string>("Token")
.HasColumnType("nvarchar(max)")
.HasAnnotation("CaseSensitive", true);
b.HasKey("AuthTokenId");
b.HasIndex("ApplicationUserId");
b.ToTable("AuthTokens");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany() .WithMany()
.HasForeignKey("RoleId") .HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
@ -248,7 +270,8 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null) b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
@ -256,7 +279,8 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null) b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
@ -264,12 +288,14 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany() .WithMany()
.HasForeignKey("RoleId") .HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null) b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
@ -277,7 +303,22 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null) b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Teknik.IdentityServer.Models.AuthToken", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("AuthTokens")
.HasForeignKey("ApplicationUserId");
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Navigation("AuthTokens");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -15,7 +15,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Middleware\" /> <Folder Include="Middleware\" />
<Folder Include="App_Data\" />
<Folder Include="wwwroot\" /> <Folder Include="wwwroot\" />
</ItemGroup> </ItemGroup>

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -23,6 +24,8 @@ namespace Teknik.IdentityServer.Models
public DateTime LastEdit { get; set; } public DateTime LastEdit { get; set; }
public virtual ICollection<AuthToken> AuthTokens { get; set; }
public ApplicationUser() : base() public ApplicationUser() : base()
{ {
Init(); Init();
@ -41,6 +44,7 @@ namespace Teknik.IdentityServer.Models
AccountType = AccountType.Basic; AccountType = AccountType.Basic;
AccountStatus = AccountStatus.Active; AccountStatus = AccountStatus.Active;
PGPPublicKey = null; PGPPublicKey = null;
AuthTokens = new List<AuthToken>();
} }
public List<Claim> ToClaims() public List<Claim> ToClaims()

View File

@ -0,0 +1,24 @@
using System;
using System.Text.Json.Serialization;
using Teknik.Utilities.Attributes;
namespace Teknik.IdentityServer.Models
{
public class AuthToken
{
public Guid AuthTokenId { get; set; }
public string Name { get; set; }
[JsonIgnore]
public string ApplicationUserId { get; set; }
[JsonIgnore]
public virtual ApplicationUser ApplicationUser { get; set; }
[JsonIgnore]
[CaseSensitive]
public string Token { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Teknik.IdentityServer.Models.Manage
{
public class CreateAuthTokenModel
{
public string Username { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Teknik.IdentityServer.Models.Manage
{
public class DeleteAuthTokenModel
{
public string AuthTokenId { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Teknik.IdentityServer.Models.Manage
{
public class EditAuthTokenModel
{
public string AuthTokenId { get; set; }
public string Name { get; set; }
}
}

View File

@ -1090,14 +1090,25 @@
} }
}, },
{ {
"Name": "User.DeveloperSettings", "Name": "User.ClientSettings",
"HostTypes": [ "Full" ], "HostTypes": [ "Full" ],
"SubDomains": [ "account" ], "SubDomains": [ "account" ],
"Pattern": "Settings/Developer", "Pattern": "Settings/Clients",
"Area": "User", "Area": "User",
"Defaults": { "Defaults": {
"controller": "User", "controller": "User",
"action": "DeveloperSettings" "action": "ClientSettings"
}
},
{
"Name": "User.AuthTokenSettings",
"HostTypes": [ "Full" ],
"SubDomains": [ "account" ],
"Pattern": "Settings/AuthTokens",
"Area": "User",
"Defaults": {
"controller": "User",
"action": "AuthTokenSettings"
} }
}, },
{ {

View File

@ -25,7 +25,6 @@ namespace Teknik.Areas.API.V1.Controllers
public PasteAPIv1Controller(ILogger<Logger> logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { } public PasteAPIv1Controller(ILogger<Logger> logger, Config config, TeknikEntities dbContext) : base(logger, config, dbContext) { }
[HttpPost] [HttpPost]
[AllowAnonymous]
[TrackPageView] [TrackPageView]
public IActionResult Paste(PasteAPIv1Model model) public IActionResult Paste(PasteAPIv1Model model)
{ {

View File

@ -418,17 +418,6 @@ namespace Teknik.Areas.Users.Controllers
// Get the user secure info // Get the user secure info
IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, user.Username); IdentityUserInfo userInfo = await IdentityHelper.GetIdentityUserInfo(_config, user.Username);
//model.TrustedDeviceCount = user.TrustedDevices.Count;
//model.AuthTokens = new List<AuthTokenViewModel>();
//foreach (AuthToken token in user.AuthTokens)
//{
// AuthTokenViewModel tokenModel = new AuthTokenViewModel();
// tokenModel.AuthTokenId = token.AuthTokenId;
// tokenModel.Name = token.Name;
// tokenModel.LastDateUsed = token.LastDateUsed;
// model.AuthTokens.Add(tokenModel);
//}
model.PgpPublicKey = userInfo.PGPPublicKey; model.PgpPublicKey = userInfo.PGPPublicKey;
model.RecoveryEmail = userInfo.RecoveryEmail; model.RecoveryEmail = userInfo.RecoveryEmail;
@ -444,32 +433,22 @@ namespace Teknik.Areas.Users.Controllers
} }
[TrackPageView] [TrackPageView]
public async Task<IActionResult> DeveloperSettings() public async Task<IActionResult> ClientSettings()
{ {
string username = User.Identity.Name; string username = User.Identity.Name;
User user = UserHelper.GetUser(_dbContext, username); User user = UserHelper.GetUser(_dbContext, username);
if (user != null) if (user != null)
{ {
ViewBag.Title = "Developer Settings"; ViewBag.Title = "Client Settings";
ViewBag.Description = "Your " + _config.Title + " Developer Settings"; ViewBag.Description = "Your " + _config.Title + " Client Settings";
DeveloperSettingsViewModel model = new DeveloperSettingsViewModel(); ClientSettingsViewModel model = new ClientSettingsViewModel();
model.Page = "Developer"; model.Page = "Clients";
model.UserID = user.UserId; model.UserID = user.UserId;
model.Username = user.Username; model.Username = user.Username;
model.AuthTokens = new List<AuthTokenViewModel>();
model.Clients = new List<ClientViewModel>(); model.Clients = new List<ClientViewModel>();
//foreach (AuthToken token in user.AuthTokens)
//{
// AuthTokenViewModel tokenModel = new AuthTokenViewModel();
// tokenModel.AuthTokenId = token.AuthTokenId;
// tokenModel.Name = token.Name;
// tokenModel.LastDateUsed = token.LastDateUsed;
// model.AuthTokens.Add(tokenModel);
//}
Client[] clients = await IdentityHelper.GetClients(_config, username); Client[] clients = await IdentityHelper.GetClients(_config, username);
foreach (Client client in clients) foreach (Client client in clients)
@ -486,7 +465,42 @@ namespace Teknik.Areas.Users.Controllers
}); });
} }
return View("/Areas/User/Views/User/Settings/DeveloperSettings.cshtml", model); return View("/Areas/User/Views/User/Settings/ClientSettings.cshtml", model);
}
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
[TrackPageView]
public async Task<IActionResult> AuthTokenSettings()
{
string username = User.Identity.Name;
User user = UserHelper.GetUser(_dbContext, username);
if (user != null)
{
ViewBag.Title = "Auth Tokens";
ViewBag.Description = "Your " + _config.Title + " - Developer Settings";
AuthTokenSettingsViewModel model = new AuthTokenSettingsViewModel();
model.Page = "AuthTokens";
model.UserID = user.UserId;
model.Username = user.Username;
model.AuthTokens = new List<AuthTokenViewModel>();
AuthToken[] authTokens = await IdentityHelper.GetAuthTokens(_config, username);
foreach (AuthToken token in authTokens)
{
AuthTokenViewModel tokenModel = new AuthTokenViewModel();
tokenModel.AuthTokenId = token.AuthTokenId;
tokenModel.Name = token.Name;
tokenModel.LastDateUsed = token.LastUsed;
model.AuthTokens.Add(tokenModel);
}
return View("/Areas/User/Views/User/Settings/AuthTokenSettings.cshtml", model);
} }
return new StatusCodeResult(StatusCodes.Status403Forbidden); return new StatusCodeResult(StatusCodes.Status403Forbidden);
@ -704,20 +718,6 @@ namespace Teknik.Areas.Users.Controllers
} }
} }
//if (!settings.TwoFactorEnabled && (!userInfo.TwoFactorEnabled.HasValue || userInfo.TwoFactorEnabled.Value))
//{
// var result = await IdentityHelper.Disable2FA(_config, user.Username);
// if (!result.Success)
// return Json(new { error = result.Message });
//}
//UserHelper.EditAccount(_dbContext, _config, user, changePass, settings.NewPassword);
//if (!oldTwoFactor && settings.TwoFactorEnabled)
//{
// return Json(new { result = new { checkAuth = true, key = newKey, qrUrl = Url.SubRouteUrl("account", "User.Action", new { action = "GenerateAuthQrCode", key = newKey }) } });
//}
return Json(new { result = true }); return Json(new { result = true });
} }
return Json(new { error = "User does not exist" }); return Json(new { error = "User does not exist" });
@ -1124,33 +1124,32 @@ namespace Teknik.Areas.Users.Controllers
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public IActionResult ClearTrustedDevices() public async Task<IActionResult> CreateAuthToken(string name, [FromServices] ICompositeViewEngine viewEngine)
{ {
try try
{ {
User user = UserHelper.GetUser(_dbContext, User.Identity.Name); if (string.IsNullOrEmpty(name))
if (user != null) return Json(new { error = "You must enter an auth token name" });
{
//if (user.SecuritySettings.AllowTrustedDevices)
//{
// // let's clear the trusted devices
// user.TrustedDevices.Clear();
// List<TrustedDevice> foundDevices = _dbContext.TrustedDevices.Where(d => d.UserId == user.UserId).ToList();
// if (foundDevices != null)
// {
// foreach (TrustedDevice device in foundDevices)
// {
// _dbContext.TrustedDevices.Remove(device);
// }
// }
// _dbContext.Entry(user).State = EntityState.Modified;
// _dbContext.SaveChanges();
// return Json(new { result = true }); // Validate the code with the identity server
//} var result = await IdentityHelper.CreateAuthToken(
return Json(new { error = "User does not allow trusted devices" }); _config,
User.Identity.Name,
name);
if (result.Success)
{
var authToken = (JObject)result.Data;
AuthTokenViewModel model = new AuthTokenViewModel();
model.AuthTokenId = authToken["authTokenId"].ToString();
model.Name = name;
string renderedView = await RenderPartialViewToString(viewEngine, "~/Areas/User/Views/User/Settings/AuthTokenView.cshtml", model);
return Json(new { result = true, authTokenId = model.AuthTokenId, token = authToken["token"].ToString(), html = renderedView });
} }
return Json(new { error = "User does not exist" }); return Json(new { error = result.Message });
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1160,37 +1159,55 @@ namespace Teknik.Areas.Users.Controllers
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> GenerateToken(string name, [FromServices] ICompositeViewEngine viewEngine) public async Task<IActionResult> GetAuthToken(string authTokenId)
{
AuthToken foundAuthToken = await IdentityHelper.GetAuthToken(_config, authTokenId);
if (foundAuthToken != null)
{
AuthTokenViewModel model = new AuthTokenViewModel()
{
AuthTokenId = foundAuthToken.AuthTokenId.ToString(),
Name = foundAuthToken.Name
};
return Json(new { result = true, authToken = model });
}
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditAuthToken(string authTokenId, string name, [FromServices] ICompositeViewEngine viewEngine)
{ {
try try
{ {
User user = UserHelper.GetUser(_dbContext, User.Identity.Name); if (string.IsNullOrEmpty(name))
if (user != null) return Json(new { error = "You must enter an auth token name" });
AuthToken foundAuthToken = await IdentityHelper.GetAuthToken(_config, authTokenId);
if (foundAuthToken == null)
return Json(new { error = "Auth Token does not exist" });
// Validate the code with the identity server
var result = await IdentityHelper.EditAuthToken(
_config,
authTokenId,
name);
if (result.Success)
{ {
//string newTokenStr = UserHelper.GenerateAuthToken(_dbContext, user.Username); var authToken = (JObject)result.Data;
//if (!string.IsNullOrEmpty(newTokenStr)) AuthTokenViewModel model = new AuthTokenViewModel();
//{ model.AuthTokenId = authTokenId;
// AuthToken token = new AuthToken(); model.Name = name;
// token.UserId = user.UserId;
// token.HashedToken = SHA256.Hash(newTokenStr);
// token.Name = name;
// _dbContext.AuthTokens.Add(token); string renderedView = await RenderPartialViewToString(viewEngine, "~/Areas/User/Views/User/Settings/AuthTokenView.cshtml", model);
// _dbContext.SaveChanges();
// AuthTokenViewModel model = new AuthTokenViewModel(); return Json(new { result = true, authTokenId = authTokenId, html = renderedView });
// model.AuthTokenId = token.AuthTokenId;
// model.Name = token.Name;
// model.LastDateUsed = token.LastDateUsed;
// string renderedView = await RenderPartialViewToString(viewEngine, "~/Areas/User/Views/User/Settings/AuthToken.cshtml", model);
// return Json(new { result = new { token = newTokenStr, html = renderedView } });
//}
return Json(new { error = "Unable to generate Auth Token" });
} }
return Json(new { error = "User does not exist" }); return Json(new { error = result.Message });
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -1200,85 +1217,19 @@ namespace Teknik.Areas.Users.Controllers
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public IActionResult RevokeAllTokens() public async Task<IActionResult> DeleteAuthToken(string authTokenId)
{ {
try try
{ {
User user = UserHelper.GetUser(_dbContext, User.Identity.Name); var result = await IdentityHelper.DeleteAuthToken(_config, authTokenId);
if (user != null) if (result.Success)
{ {
//user.AuthTokens.Clear();
//List<AuthToken> foundTokens = _dbContext.AuthTokens.Where(d => d.UserId == user.UserId).ToList();
//if (foundTokens != null)
//{
// foreach (AuthToken token in foundTokens)
// {
// _dbContext.AuthTokens.Remove(token);
// }
//}
_dbContext.Entry(user).State = EntityState.Modified;
_dbContext.SaveChanges();
return Json(new { result = true }); return Json(new { result = true });
} }
return Json(new { error = "User does not exist" }); else
}
catch (Exception ex)
{
return Json(new { error = ex.GetFullMessage(true) });
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult EditTokenName(int tokenId, string name)
{
try
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
{ {
//AuthToken foundToken = _dbContext.AuthTokens.Where(d => d.UserId == user.UserId && d.AuthTokenId == tokenId).FirstOrDefault(); return Json(new { error = result.Message });
//if (foundToken != null)
//{
// foundToken.Name = name;
// _dbContext.Entry(foundToken).State = EntityState.Modified;
// _dbContext.SaveChanges();
// return Json(new { result = new { name = name } });
//}
return Json(new { error = "Authentication Token does not exist" });
} }
return Json(new { error = "User does not exist" });
}
catch (Exception ex)
{
return Json(new { error = ex.GetFullMessage(true) });
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DeleteToken(int tokenId)
{
try
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
{
//AuthToken foundToken = _dbContext.AuthTokens.Where(d => d.UserId == user.UserId && d.AuthTokenId == tokenId).FirstOrDefault();
//if (foundToken != null)
//{
// _dbContext.AuthTokens.Remove(foundToken);
// user.AuthTokens.Remove(foundToken);
// _dbContext.Entry(user).State = EntityState.Modified;
// _dbContext.SaveChanges();
// return Json(new { result = true });
//}
return Json(new { error = "Authentication Token does not exist" });
}
return Json(new { error = "User does not exist" });
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -0,0 +1,16 @@
using System;
using Teknik.Utilities.Attributes;
namespace Teknik.Areas.Users.Models
{
public class AuthToken
{
public string AuthTokenId { get; set; }
public string Name { get; set; }
public string Token { get; set; }
public DateTime? LastUsed { get; set; }
}
}

View File

@ -10,6 +10,8 @@ namespace Teknik.Areas.Users.Models
{ {
public class IdentityUserInfo public class IdentityUserInfo
{ {
public string Username { get; set; }
public DateTime? CreationDate { get; set; } public DateTime? CreationDate { get; set; }
public DateTime? LastSeen { get; set; } public DateTime? LastSeen { get; set; }
@ -30,6 +32,10 @@ namespace Teknik.Areas.Users.Models
public IdentityUserInfo(IEnumerable<Claim> claims) public IdentityUserInfo(IEnumerable<Claim> claims)
{ {
if (claims.FirstOrDefault(c => c.Type == "username") != null)
{
RecoveryEmail = claims.FirstOrDefault(c => c.Type == "username").Value;
}
if (claims.FirstOrDefault(c => c.Type == "creation-date") != null) if (claims.FirstOrDefault(c => c.Type == "creation-date") != null)
{ {
if (DateTime.TryParse(claims.FirstOrDefault(c => c.Type == "creation-date").Value, out var dateTime)) if (DateTime.TryParse(claims.FirstOrDefault(c => c.Type == "creation-date").Value, out var dateTime))
@ -72,6 +78,10 @@ namespace Teknik.Areas.Users.Models
public IdentityUserInfo(JObject info) public IdentityUserInfo(JObject info)
{ {
if (info["username"] != null)
{
Username = info["username"].ToString();
}
if (info["creation-date"] != null) if (info["creation-date"] != null)
{ {
if (DateTime.TryParse(info["creation-date"].ToString(), out var dateTime)) if (DateTime.TryParse(info["creation-date"].ToString(), out var dateTime))

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Teknik.Areas.Users.Models; using Teknik.Areas.Users.Models;
using Teknik.Configuration; using Teknik.Configuration;
@ -171,6 +172,30 @@ namespace Teknik.Areas.Users.Utility
throw new Exception(result.Message); throw new Exception(result.Message);
} }
public static async Task<IdentityUserInfo> GetIdentityUserInfoByToken(Config config, string token)
{
var manageUrl = CreateUrl(config, $"Manage/GetUserInfoByAuthToken?authToken={token}");
var result = await Get(config, manageUrl);
if (result.Success)
{
return new IdentityUserInfo((JObject)result.Data);
}
throw new Exception(result.Message);
}
public static async Task<Claim[]> GetIdentityUserClaims(Config config, string username)
{
var manageUrl = CreateUrl(config, $"Manage/GetUserClaims?username={username}");
var result = await Get(config, manageUrl);
if (result.Success)
{
return ((JArray)result.Data).ToObject<Claim[]>();
}
throw new Exception(result.Message);
}
public static async Task<bool> CheckPassword(Config config, string username, string password) public static async Task<bool> CheckPassword(Config config, string username, string password)
{ {
var manageUrl = CreateUrl(config, $"Manage/CheckPassword"); var manageUrl = CreateUrl(config, $"Manage/CheckPassword");
@ -446,5 +471,67 @@ namespace Teknik.Areas.Users.Utility
}); });
return response; return response;
} }
public static async Task<AuthToken> GetAuthToken(Config config, string authTokenId)
{
var manageUrl = CreateUrl(config, $"Manage/GetAuthToken?authTokenId={authTokenId}");
var result = await Get(config, manageUrl);
if (result.Success)
{
return ((JObject)result.Data).ToObject<AuthToken>();
}
throw new Exception(result.Message);
}
public static async Task<AuthToken[]> GetAuthTokens(Config config, string username)
{
var manageUrl = CreateUrl(config, $"Manage/GetAuthTokens?username={username}");
var result = await Get(config, manageUrl);
if (result.Success)
{
return ((JArray)result.Data).ToObject<AuthToken[]>();
}
throw new Exception(result.Message);
}
public static async Task<IdentityResult> CreateAuthToken(Config config, string username, string name)
{
var manageUrl = CreateUrl(config, $"Manage/CreateAuthToken");
var response = await Post(config, manageUrl,
new
{
username = username,
name = name
});
return response;
}
public static async Task<IdentityResult> EditAuthToken(Config config, string authTokenId, string name)
{
var manageUrl = CreateUrl(config, $"Manage/EditAuthToken");
var response = await Post(config, manageUrl,
new
{
authTokenId = authTokenId,
name = name
});
return response;
}
public static async Task<IdentityResult> DeleteAuthToken(Config config, string authTokenId)
{
var manageUrl = CreateUrl(config, $"Manage/DeleteAuthToken");
var response = await Post(config, manageUrl,
new
{
authTokenId = authTokenId
});
return response;
}
} }
} }

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Teknik.Areas.Users.ViewModels
{
public class AuthTokenSettingsViewModel : SettingsViewModel
{
public List<AuthTokenViewModel> AuthTokens { get; set; }
public AuthTokenSettingsViewModel()
{
AuthTokens = new List<AuthTokenViewModel>();
}
}
}

View File

@ -8,7 +8,7 @@ namespace Teknik.Areas.Users.ViewModels
{ {
public class AuthTokenViewModel : ViewModelBase public class AuthTokenViewModel : ViewModelBase
{ {
public int AuthTokenId { get; set; } public string AuthTokenId { get; set; }
public string Name { get; set; } public string Name { get; set; }

View File

@ -5,15 +5,12 @@ using System.Threading.Tasks;
namespace Teknik.Areas.Users.ViewModels namespace Teknik.Areas.Users.ViewModels
{ {
public class DeveloperSettingsViewModel : SettingsViewModel public class ClientSettingsViewModel : SettingsViewModel
{ {
public List<AuthTokenViewModel> AuthTokens { get; set; }
public List<ClientViewModel> Clients { get; set; } public List<ClientViewModel> Clients { get; set; }
public DeveloperSettingsViewModel() public ClientSettingsViewModel()
{ {
AuthTokens = new List<AuthTokenViewModel>();
Clients = new List<ClientViewModel>(); Clients = new List<ClientViewModel>();
} }
} }

View File

@ -0,0 +1,76 @@
@model Teknik.Areas.Users.ViewModels.AuthTokenSettingsViewModel
@using Teknik.Areas.Users.ViewModels
@{
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
}
<script>
var createAuthTokenURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "CreateAuthToken" })';
var editAuthTokenURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "EditAuthToken" })';
var deleteAuthTokenURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "DeleteAuthToken" })';
var getAuthTokenURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "GetAuthToken" })';
</script>
<div class="row">
<div class="col-sm-12">
<h2>Auth Tokens</h2>
<hr />
</div>
</div>
<div class="row">
<div class="col-sm-12">
<span class="pull-right"><button type="button" class="btn btn-default" id="createAuthToken">Create Auth Token</button></span>
<div class="clearfix"></div>
<br />
<div id="authTokens">
<ul class="list-group" id="authTokenList">
@if (Model.AuthTokens.Any())
{
foreach (AuthTokenViewModel authToken in Model.AuthTokens)
{
@await Html.PartialAsync("Settings/AuthTokenView", authToken)
}
}
else
{
<li class="list-group-item text-center" id="noAuthTokens">No Auth Tokens</li>
}
</ul>
</div>
</div>
</div>
<div class="modal fade" id="authTokenModal" tabindex="-1" role="dialog" aria-labelledby="authTokenModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header modal-header-default">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="authTokenModalLabel">Auth Token Information</h4>
</div>
<div class="modal-body">
<input type="hidden" class="form-control" id="authTokenId" name="authTokenId" />
<div class="row">
<div class="col-sm-12 text-center">
<div id="authTokenStatus">
</div>
</div>
</div>
<div class="form-group">
<label for="authTokenName">Auth Token Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="authTokenName" name="authTokenName" data-val-required="The Auth Token Name field is required." data-val="true" />
</div>
<div class="row">
<div class="col-sm-12">
<button class="btn btn-primary pull-right hidden authTokenSubmit" id="authTokenCreateSubmit" type="submit" name="authTokenCreateSubmit">Create Auth Token</button>
<button class="btn btn-primary pull-right hidden authTokenSubmit" id="authTokenEditSubmit" type="submit" name="authTokenEditSubmit">Save Auth Token</button>
</div>
</div>
</div>
</div>
</div>
</div>
<bundle src="js/user.settings.authToken.min.js" append-version="true"></bundle>

View File

@ -2,8 +2,8 @@
<li class="list-group-item" id="authToken_@Model.AuthTokenId"> <li class="list-group-item" id="authToken_@Model.AuthTokenId">
<div class="btn-group btn-group-sm pull-right" role="group" aria-label="..."> <div class="btn-group btn-group-sm pull-right" role="group" aria-label="...">
<button type="button" class="btn btn-default editAuthToken" id="editAuthToken_@Model.AuthTokenId" data-authId="@Model.AuthTokenId">Edit</button> <button type="button" class="btn btn-default editAuthToken" id="editAuthToken_@Model.AuthTokenId" data-authTokenId="@Model.AuthTokenId">Edit</button>
<button type="button" class="btn btn-danger text-danger deleteAuthToken" id="deleteAuthToken_@Model.AuthTokenId" data-authId="@Model.AuthTokenId">Delete</button> <button type="button" class="btn btn-danger text-danger deleteAuthToken" id="deleteAuthToken_@Model.AuthTokenId" data-authTokenId="@Model.AuthTokenId">Delete</button>
</div> </div>
<h4 class="list-group-item-heading" id="authTokenName_@Model.AuthTokenId">@Model.Name</h4> <h4 class="list-group-item-heading" id="authTokenName_@Model.AuthTokenId">@Model.Name</h4>
@if (Model.LastDateUsed.HasValue) @if (Model.LastDateUsed.HasValue)

View File

@ -1,4 +1,4 @@
@model Teknik.Areas.Users.ViewModels.DeveloperSettingsViewModel @model Teknik.Areas.Users.ViewModels.ClientSettingsViewModel
@using Teknik.Areas.Users.ViewModels @using Teknik.Areas.Users.ViewModels
@ -6,7 +6,7 @@
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml"; Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
} }
<bundle src="css/user.settings.developer.min.css" append-version="true"></bundle> <bundle src="css/user.settings.client.min.css" append-version="true"></bundle>
<script> <script>
var createClientURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "CreateClient" })'; var createClientURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "CreateClient" })';
@ -133,4 +133,4 @@
</div> </div>
</div> </div>
<bundle src="js/user.settings.developer.min.js" append-version="true"></bundle> <bundle src="js/user.settings.client.min.js" append-version="true"></bundle>

View File

@ -14,7 +14,7 @@
<!--left col--> <!--left col-->
<div class="panel panel-default"> <div class="panel panel-default">
<ul class="list-group"> <ul class="list-group">
<div class="panel-heading text-center"><strong>Personal Settings</strong></div> <div class="panel-heading text-center"><h5>Personal Settings</h5></div>
<a href="@Url.SubRouteUrl("account", "User.ProfileSettings")" class="list-group-item @(Model.Page == "Profile" ? "active" : string.Empty)">Profile</a> <a href="@Url.SubRouteUrl("account", "User.ProfileSettings")" class="list-group-item @(Model.Page == "Profile" ? "active" : string.Empty)">Profile</a>
<a href="@Url.SubRouteUrl("account", "User.AccountSettings")" class="list-group-item @(Model.Page == "Account" ? "active" : string.Empty)">Account</a> <a href="@Url.SubRouteUrl("account", "User.AccountSettings")" class="list-group-item @(Model.Page == "Account" ? "active" : string.Empty)">Account</a>
@if (Config.BillingConfig.Enabled) @if (Config.BillingConfig.Enabled)
@ -29,7 +29,9 @@
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<ul class="list-group"> <ul class="list-group">
<a href="@Url.SubRouteUrl("account", "User.DeveloperSettings")" class="list-group-item @(Model.Page == "Developer" ? "active" : string.Empty)">Developer Settings</a> <div class="panel-heading text-center"><h5>Developer Settings</h5></div>
<a href="@Url.SubRouteUrl("account", "User.ClientSettings")" class="list-group-item @(Model.Page == "Clients" ? "active" : string.Empty)">Clients</a>
<a href="@Url.SubRouteUrl("account", "User.AuthTokenSettings")" class="list-group-item @(Model.Page == "AuthTokens" ? "active" : string.Empty)">Auth Tokens</a>
</ul> </ul>
</div> </div>
</div><!--/col-2--> </div><!--/col-2-->

View File

@ -0,0 +1,196 @@
/* globals createAuthTokenURL, getAuthTokenURL, editAuthTokenURL, deleteAuthTokenURL */
$(document).ready(function () {
$('#AuthTokenModal').on('shown.bs.modal', function () {
$("#authTokenStatus").css('display', 'none', 'important');
$("#authTokenStatus").html('');
$('#authTokenName').focus();
});
$('#authTokenModal').on('hide.bs.modal', function () {
$("#authTokenStatus").css('display', 'none', 'important');
$("#authTokenStatus").html('');
$(this).find('#authTokenCreateSubmit').addClass('hidden');
$(this).find('#authTokenEditSubmit').addClass('hidden');
clearInputs('#authTokenModal');
});
$('#authTokenModal').find('#authTokenCreateSubmit').click(createAuthToken);
$('#authTokenModal').find('#authTokenEditSubmit').click(editAuthTokenSave);
$("#createAuthToken").click(function () {
$('#authTokenModal').find('#authTokenCreateSubmit').removeClass('hidden');
$('#authTokenModal').find('#authTokenCreateSubmit').text('Create Auth Token');
$('#authTokenModal').modal('show');
});
$(".editAuthToken").click(function () {
var authTokenId = $(this).attr("data-authTokenId");
editAuthToken(authTokenId);
});
$(".deleteAuthToken").click(function () {
var authTokenId = $(this).attr("data-authTokenId");
deleteAuthToken(authTokenId);
});
});
function createAuthToken() {
saveAuthTokenInfo(createAuthTokenURL, 'Create Auth Token', 'Creating Auth Token...', function (response) {
$('#authTokenModal').modal('hide');
var dialog = bootbox.dialog({
closeButton: false,
buttons: {
close: {
label: 'Close',
className: 'btn-primary',
callback: function () {
if ($('#noAuthTokens')) {
$('#noAuthTokens').remove();
}
var item = $(response.html);
processAuthTokenItem(item);
$('#authTokenList').append(item);
}
}
},
title: "Auth Token Secret",
message: '<label for="authTokenSecret">Make sure to copy your auth token now.<br />You won\'t be able to see it again!</label><input type="text" class="form-control" id="authTokenSecret" value="' + response.token + '">',
});
dialog.init(function () {
dialog.find('#authTokenSecret').click(function () {
$(this).select();
});
});
});
}
function processAuthTokenItem(item) {
item.find('.editAuthToken').click(function () {
var authTokenId = $(this).attr("data-authTokenId");
editAuthToken(authTokenId);
});
item.find('.deleteAuthToken').click(function () {
var authTokenId = $(this).attr("data-authTokenId");
deleteAuthToken(authTokenId);
});
}
function editAuthToken(authTokenId) {
disableButton('.editAuthToken[data-authTokenId="' + authTokenId + '"]', 'Loading...');
$.ajax({
type: "POST",
url: getAuthTokenURL,
data: AddAntiForgeryToken({ authTokenId: authTokenId }),
success: function (data) {
if (data.result) {
$('#authTokenModal').find('#authTokenId').val(data.authToken.authTokenId);
$('#authTokenModal').find('#authTokenName').val(data.authToken.name);
$('#authTokenModal').find('#authTokenEditSubmit').removeClass('hidden');
$('#authTokenModal').find('#authTokenEditSubmit').text('Save Auth Token');
$('#authTokenModal').modal('show');
enableButton('.editAuthToken[data-authTokenId="' + authTokenId + '"]', 'Edit');
}
}
});
}
function editAuthTokenSave() {
saveAuthTokenInfo(editAuthTokenURL, 'Save Auth Token', 'Saving Auth Token...', function (response) {
$('#authToken_' + response.authTokenId).replaceWith(response.html);
processAuthTokenItem($('#authToken_' + response.authTokenId));
$('#authTokenModal').modal('hide');
$("#top_msg").css('display', 'inline', 'important');
$("#top_msg").html('<div class="alert alert-success alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>Successfully Saved Auth Token</div>');
});
}
function deleteAuthToken(authTokenId) {
disableButton('.deleteAuthToken[data-authTokenId="' + authTokenId + '"]', 'Deleting...');
bootbox.confirm({
message: "<h2>Are you sure you want to delete this auth token?</h2><br /><br />This is <b>irreversable</b> and all applications using this token will stop working.",
buttons: {
confirm: {
label: 'Delete',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-default'
}
},
callback: function (result) {
if (result) {
$.ajax({
type: "POST",
url: deleteAuthTokenURL,
data: AddAntiForgeryToken({ authTokenId: authTokenId }),
success: function (response) {
if (response.result) {
$('#authToken_' + authTokenId).remove();
if ($('#authTokenList li').length <= 0) {
$('#authTokenList').html('<li class="list-group-item text-center" id="noAuthTokens">No Auth Tokens</li>');
}
}
else {
$("#top_msg").css('display', 'inline', 'important');
$("#top_msg").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(response) + '</div>');
}
}
}).always(function () {
enableButton('.deleteAuthToken[data-authTokenId="' + authTokenId + '"]', 'Delete');
});
} else {
enableButton('.deleteAuthToken[data-authTokenId="' + authTokenId + '"]', 'Delete');
}
}
});
}
function saveAuthTokenInfo(url, submitText, submitActionText, callback) {
var authTokenId, name;
disableButton('.authTokenSubmit', submitActionText);
authTokenId = $('#authTokenModal').find('#authTokenId').val();
name = $('#authTokenModal').find('#authTokenName').val();
$.ajax({
type: "POST",
url: url,
data: AddAntiForgeryToken({ authTokenId: authTokenId, name: name }),
success: function (response) {
if (response.result) {
if (callback) {
callback(response);
}
}
else {
$("#authTokenStatus").css('display', 'inline', 'important');
$("#authTokenStatus").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(response) + '</div>');
}
},
error: function (response) {
$("#authTokenStatus").css('display', 'inline', 'important');
$("#authTokenStatus").html('<div class="alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + parseErrorMessage(response.responseText) + '</div>');
}
}).always(function () {
enableButton('.authTokenSubmit', submitText);
});
}

View File

@ -0,0 +1,97 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Areas.Users.Utility;
using Teknik.Configuration;
using Teknik.Data;
namespace Teknik.Security
{
public class AuthTokenAuthenticationHandler : AuthenticationHandler<AuthTokenSchemeOptions>
{
private readonly TeknikEntities _db;
private readonly Config _config;
public AuthTokenAuthenticationHandler(
IOptionsMonitor<AuthTokenSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
TeknikEntities dbContext,
Config config)
: base(options, logger, encoder, clock)
{
_db = dbContext;
_config = config;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// validation comes in here
if (!Request.Headers.ContainsKey(HeaderNames.Authorization))
{
return AuthenticateResult.Fail("Header Not Found.");
}
var header = Request.Headers[HeaderNames.Authorization].ToString();
var tokenMatch = Regex.Match(header, $"AuthToken (?<token>.*)");
if (tokenMatch.Success)
{
// the token is captured in this group
// as declared in the Regex
var token = tokenMatch.Groups["token"].Value;
IdentityUserInfo userInfo = null;
try
{
userInfo = await IdentityHelper.GetIdentityUserInfoByToken(_config, token);
}
catch (Exception ex)
{
Console.WriteLine("Exception Occured while Deserializing: " + ex);
return AuthenticateResult.Fail("TokenParseException");
}
// success branch
// generate authTicket
// authenticate the request
if (userInfo != null)
{
// create claims array from the model
var claims = new[]
{
new Claim(ClaimTypes.Name, userInfo.Username),
new Claim("scope", "teknik-api.read"),
new Claim("scope", "teknik-api.write")
};
// generate claimsIdentity on the name of the class
var claimsIdentity = new ClaimsIdentity(claims, nameof(AuthTokenAuthenticationHandler));
// generate AuthenticationTicket from the Identity
// and current authentication scheme
var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// pass on the ticket to the middleware
return AuthenticateResult.Success(ticket);
}
}
// failure branch
// return failure
// with an optional message
return AuthenticateResult.Fail("Invalid Authentication Token");
}
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Teknik.Security
{
public class AuthTokenSchemeOptions : AuthenticationSchemeOptions
{
}
}

View File

@ -227,33 +227,34 @@ namespace Teknik
}; };
options.Events.OnRemoteFailure = HandleOnRemoteFailure; options.Events.OnRemoteFailure = HandleOnRemoteFailure;
}); })
.AddScheme<AuthTokenSchemeOptions, AuthTokenAuthenticationHandler>("AuthToken", "AuthToken", options => { });
services.AddAuthorization(options => services.AddAuthorization(options =>
{ {
options.AddPolicy("FullAPI", p => options.AddPolicy("FullAPI", p =>
{ {
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken"); p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.read"); p.RequireScope("teknik-api.read");
p.RequireScope("teknik-api.write"); p.RequireScope("teknik-api.write");
}); });
options.AddPolicy("ReadAPI", p => options.AddPolicy("ReadAPI", p =>
{ {
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken"); p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.read"); p.RequireScope("teknik-api.read");
}); });
options.AddPolicy("WriteAPI", p => options.AddPolicy("WriteAPI", p =>
{ {
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken"); p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.write"); p.RequireScope("teknik-api.write");
}); });
options.AddPolicy("AnyAPI", p => options.AddPolicy("AnyAPI", p =>
{ {
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken"); p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.read", "teknik-api.write"); p.RequireScope("teknik-api.read", "teknik-api.write");
}); });
}); });

View File

@ -274,15 +274,21 @@
] ]
}, },
{ {
"outputFileName": "./wwwroot/js/user.settings.developer.min.js", "outputFileName": "./wwwroot/js/user.settings.client.min.js",
"inputFiles": [ "inputFiles": [
"./wwwroot/js/app/User/DeveloperSettings.js" "./wwwroot/js/app/User/ClientSettings.js"
] ]
}, },
{ {
"outputFileName": "./wwwroot/css/user.settings.developer.min.css", "outputFileName": "./wwwroot/css/user.settings.client.min.css",
"inputFiles": [ "inputFiles": [
"./wwwroot/css/app/User/Settings/Developer.css" "./wwwroot/css/app/User/Settings/Client.css"
]
},
{
"outputFileName": "./wwwroot/js/user.settings.authToken.min.js",
"inputFiles": [
"./wwwroot/js/app/User/AuthTokenSettings.js"
] ]
}, },
{ {

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Teknik.Utilities.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class CaseSensitiveAttribute : Attribute
{
public CaseSensitiveAttribute()
{
IsEnabled = true;
}
public bool IsEnabled { get; set; }
}
}

View File

@ -16,6 +16,8 @@ namespace Teknik.Utilities
public const string NONCE_KEY = "Nonce"; public const string NONCE_KEY = "Nonce";
public const string AUTH_TOKEN_MATCH = "AuthToken";
public const string NullIpAddress = "::1"; public const string NullIpAddress = "::1";
} }
} }