Browse Source

Added Auth Tokens for direct API access

tags/5.2.0
Teknikode 1 month ago
parent
commit
2233d6c3be
35 changed files with 1344 additions and 186 deletions
  1. 4
    0
      IdentityServer/App_Data/version.json
  2. 4
    0
      IdentityServer/App_Data/version.template.json
  3. 29
    0
      IdentityServer/ApplicationDbContext.cs
  4. 153
    0
      IdentityServer/Controllers/ManageController.cs
  5. 328
    0
      IdentityServer/Data/Migrations/ApplicationDb/20211206044736_AuthToken.Designer.cs
  6. 42
    0
      IdentityServer/Data/Migrations/ApplicationDb/20211206044736_AuthToken.cs
  7. 50
    9
      IdentityServer/Data/Migrations/ApplicationDb/ApplicationDbContextModelSnapshot.cs
  8. 0
    1
      IdentityServer/IdentityServer.csproj
  9. 4
    0
      IdentityServer/Models/ApplicationUser.cs
  10. 24
    0
      IdentityServer/Models/AuthToken.cs
  11. 13
    0
      IdentityServer/Models/Manage/CreateAuthTokenModel.cs
  12. 12
    0
      IdentityServer/Models/Manage/DeleteAuthTokenModel.cs
  13. 10
    0
      IdentityServer/Models/Manage/EditAuthTokenModel.cs
  14. 14
    3
      Teknik/App_Data/endpointMappings.json
  15. 0
    1
      Teknik/Areas/API/V1/Controllers/PasteAPIv1Controller.cs
  16. 101
    150
      Teknik/Areas/User/Controllers/UserController.cs
  17. 16
    0
      Teknik/Areas/User/Models/AuthToken.cs
  18. 10
    0
      Teknik/Areas/User/Models/IdentityUserInfo.cs
  19. 87
    0
      Teknik/Areas/User/Utility/IdentityHelper.cs
  20. 14
    0
      Teknik/Areas/User/ViewModels/AuthTokenSettingsViewModel.cs
  21. 1
    1
      Teknik/Areas/User/ViewModels/AuthTokenViewModel.cs
  22. 2
    5
      Teknik/Areas/User/ViewModels/ClientSettingsViewModel.cs
  23. 76
    0
      Teknik/Areas/User/Views/User/Settings/AuthTokenSettings.cshtml
  24. 2
    2
      Teknik/Areas/User/Views/User/Settings/AuthTokenView.cshtml
  25. 3
    3
      Teknik/Areas/User/Views/User/Settings/ClientSettings.cshtml
  26. 4
    2
      Teknik/Areas/User/Views/User/Settings/Settings.cshtml
  27. 0
    0
      Teknik/Content/User/Settings/Client.css
  28. 196
    0
      Teknik/Scripts/User/AuthTokenSettings.js
  29. 0
    0
      Teknik/Scripts/User/ClientSettings.js
  30. 97
    0
      Teknik/Security/AuthTokenAuthenticationHandler.cs
  31. 13
    0
      Teknik/Security/AuthTokenSchemeOptions.cs
  32. 6
    5
      Teknik/Startup.cs
  33. 10
    4
      Teknik/bundleconfig.json
  34. 17
    0
      Utilities/Attributes/EntityAttribute.cs
  35. 2
    0
      Utilities/Constants.cs

+ 4
- 0
IdentityServer/App_Data/version.json View File

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

+ 4
- 0
IdentityServer/App_Data/version.template.json View File

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

+ 29
- 0
IdentityServer/ApplicationDbContext.cs View File

@@ -1,11 +1,40 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Teknik.IdentityServer.Models;
using Teknik.Utilities.Attributes;

namespace Teknik.IdentityServer
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<AuthToken> AuthTokens { get; set; }

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);
}
}
}
}
}
}

+ 153
- 0
IdentityServer/Controllers/ManageController.cs View File

@@ -139,6 +139,36 @@ namespace Teknik.IdentityServer.Controllers
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]
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." });
}

[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)
{
var result = new StringBuilder();
@@ -709,6 +842,26 @@ namespace Teknik.IdentityServer.Controllers
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)
{
if (string.IsNullOrEmpty(username))

+ 328
- 0
IdentityServer/Data/Migrations/ApplicationDb/20211206044736_AuthToken.Designer.cs 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
}
}
}

+ 42
- 0
IdentityServer/Data/Migrations/ApplicationDb/20211206044736_AuthToken.cs 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");
}
}
}

+ 50
- 9
IdentityServer/Data/Migrations/ApplicationDb/ApplicationDbContextModelSnapshot.cs View File

@@ -16,13 +16,12 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024")
.HasAnnotation("ProductVersion", "5.0.7")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");

b.Property<string>("ConcurrencyStamp")
@@ -154,7 +153,6 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");

b.Property<int>("AccessFailedCount")
@@ -235,12 +233,36 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
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);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

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)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

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)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

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)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("Teknik.IdentityServer.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

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)
.WithMany()
.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
}

+ 0
- 1
IdentityServer/IdentityServer.csproj View File

@@ -15,7 +15,6 @@

<ItemGroup>
<Folder Include="Middleware\" />
<Folder Include="App_Data\" />
<Folder Include="wwwroot\" />
</ItemGroup>


+ 4
- 0
IdentityServer/Models/ApplicationUser.cs View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
@@ -23,6 +24,8 @@ namespace Teknik.IdentityServer.Models

public DateTime LastEdit { get; set; }

public virtual ICollection<AuthToken> AuthTokens { get; set; }

public ApplicationUser() : base()
{
Init();
@@ -41,6 +44,7 @@ namespace Teknik.IdentityServer.Models
AccountType = AccountType.Basic;
AccountStatus = AccountStatus.Active;
PGPPublicKey = null;
AuthTokens = new List<AuthToken>();
}

public List<Claim> ToClaims()

+ 24
- 0
IdentityServer/Models/AuthToken.cs 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; }
}
}

+ 13
- 0
IdentityServer/Models/Manage/CreateAuthTokenModel.cs 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; }
}
}

+ 12
- 0
IdentityServer/Models/Manage/DeleteAuthTokenModel.cs 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; }
}
}

+ 10
- 0
IdentityServer/Models/Manage/EditAuthTokenModel.cs 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; }
}
}

+ 14
- 3
Teknik/App_Data/endpointMappings.json View File

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

+ 0
- 1
Teknik/Areas/API/V1/Controllers/PasteAPIv1Controller.cs 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) { }

[HttpPost]
[AllowAnonymous]
[TrackPageView]
public IActionResult Paste(PasteAPIv1Model model)
{

+ 101
- 150
Teknik/Areas/User/Controllers/UserController.cs View File

@@ -418,17 +418,6 @@ namespace Teknik.Areas.Users.Controllers

// Get the user secure info
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.RecoveryEmail = userInfo.RecoveryEmail;
@@ -444,32 +433,22 @@ namespace Teknik.Areas.Users.Controllers
}

[TrackPageView]
public async Task<IActionResult> DeveloperSettings()
public async Task<IActionResult> ClientSettings()
{
string username = User.Identity.Name;
User user = UserHelper.GetUser(_dbContext, username);

if (user != null)
{
ViewBag.Title = "Developer Settings";
ViewBag.Description = "Your " + _config.Title + " Developer Settings";
ViewBag.Title = "Client Settings";
ViewBag.Description = "Your " + _config.Title + " Client Settings";

DeveloperSettingsViewModel model = new DeveloperSettingsViewModel();
model.Page = "Developer";
ClientSettingsViewModel model = new ClientSettingsViewModel();
model.Page = "Clients";
model.UserID = user.UserId;
model.Username = user.Username;

model.AuthTokens = new List<AuthTokenViewModel>();
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);
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);
@@ -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 { error = "User does not exist" });
@@ -1124,73 +1124,32 @@ namespace Teknik.Areas.Users.Controllers

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ClearTrustedDevices()
public async Task<IActionResult> CreateAuthToken(string name, [FromServices] ICompositeViewEngine viewEngine)
{
try
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
{
//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 });
//}
return Json(new { error = "User does not allow trusted devices" });
}
return Json(new { error = "User does not exist" });
}
catch (Exception ex)
{
return Json(new { error = ex.GetFullMessage(true) });
}
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> GenerateToken(string name, [FromServices] ICompositeViewEngine viewEngine)
{
try
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
{
//string newTokenStr = UserHelper.GenerateAuthToken(_dbContext, user.Username);
if (string.IsNullOrEmpty(name))
return Json(new { error = "You must enter an auth token name" });

//if (!string.IsNullOrEmpty(newTokenStr))
//{
// AuthToken token = new AuthToken();
// token.UserId = user.UserId;
// token.HashedToken = SHA256.Hash(newTokenStr);
// token.Name = name;
// Validate the code with the identity server
var result = await IdentityHelper.CreateAuthToken(
_config,
User.Identity.Name,
name);

// _dbContext.AuthTokens.Add(token);
// _dbContext.SaveChanges();
if (result.Success)
{
var authToken = (JObject)result.Data;

// AuthTokenViewModel model = new AuthTokenViewModel();
// model.AuthTokenId = token.AuthTokenId;
// model.Name = token.Name;
// model.LastDateUsed = token.LastDateUsed;
AuthTokenViewModel model = new AuthTokenViewModel();
model.AuthTokenId = authToken["authTokenId"].ToString();
model.Name = name;

// string renderedView = await RenderPartialViewToString(viewEngine, "~/Areas/User/Views/User/Settings/AuthToken.cshtml", model);
string renderedView = await RenderPartialViewToString(viewEngine, "~/Areas/User/Views/User/Settings/AuthTokenView.cshtml", model);

// return Json(new { result = new { token = newTokenStr, html = renderedView } });
//}
return Json(new { error = "Unable to generate Auth Token" });
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)
{
@@ -1200,56 +1159,55 @@ namespace Teknik.Areas.Users.Controllers

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult RevokeAllTokens()
public async Task<IActionResult> GetAuthToken(string authTokenId)
{
try
AuthToken foundAuthToken = await IdentityHelper.GetAuthToken(_config, authTokenId);
if (foundAuthToken != null)
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
AuthTokenViewModel model = new AuthTokenViewModel()
{
//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();
AuthTokenId = foundAuthToken.AuthTokenId.ToString(),
Name = foundAuthToken.Name
};

return Json(new { result = true });
}
return Json(new { error = "User does not exist" });
}
catch (Exception ex)
{
return Json(new { error = ex.GetFullMessage(true) });
return Json(new { result = true, authToken = model });
}
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult EditTokenName(int tokenId, string name)
public async Task<IActionResult> EditAuthToken(string authTokenId, string name, [FromServices] ICompositeViewEngine viewEngine)
{
try
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
if (string.IsNullOrEmpty(name))
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)
{
//AuthToken foundToken = _dbContext.AuthTokens.Where(d => d.UserId == user.UserId && d.AuthTokenId == tokenId).FirstOrDefault();
//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" });
var authToken = (JObject)result.Data;

AuthTokenViewModel model = new AuthTokenViewModel();
model.AuthTokenId = authTokenId;
model.Name = name;

string renderedView = await RenderPartialViewToString(viewEngine, "~/Areas/User/Views/User/Settings/AuthTokenView.cshtml", model);

return Json(new { result = true, authTokenId = authTokenId, html = renderedView });
}
return Json(new { error = "User does not exist" });
return Json(new { error = result.Message });
}
catch (Exception ex)
{
@@ -1259,26 +1217,19 @@ namespace Teknik.Areas.Users.Controllers

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DeleteToken(int tokenId)
public async Task<IActionResult> DeleteAuthToken(string authTokenId)
{
try
{
User user = UserHelper.GetUser(_dbContext, User.Identity.Name);
if (user != null)
var result = await IdentityHelper.DeleteAuthToken(_config, authTokenId);
if (result.Success)
{
//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 { result = true });
}
else
{
return Json(new { error = result.Message });
}
return Json(new { error = "User does not exist" });
}
catch (Exception ex)
{

+ 16
- 0
Teknik/Areas/User/Models/AuthToken.cs 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; }
}
}

+ 10
- 0
Teknik/Areas/User/Models/IdentityUserInfo.cs View File

@@ -10,6 +10,8 @@ namespace Teknik.Areas.Users.Models
{
public class IdentityUserInfo
{
public string Username { get; set; }

public DateTime? CreationDate { get; set; }

public DateTime? LastSeen { get; set; }
@@ -30,6 +32,10 @@ namespace Teknik.Areas.Users.Models

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 (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)
{
if (info["username"] != null)
{
Username = info["username"].ToString();
}
if (info["creation-date"] != null)
{
if (DateTime.TryParse(info["creation-date"].ToString(), out var dateTime))

+ 87
- 0
Teknik/Areas/User/Utility/IdentityHelper.cs View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Teknik.Areas.Users.Models;
using Teknik.Configuration;
@@ -171,6 +172,30 @@ namespace Teknik.Areas.Users.Utility
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)
{
var manageUrl = CreateUrl(config, $"Manage/CheckPassword");
@@ -446,5 +471,67 @@ namespace Teknik.Areas.Users.Utility
});
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;
}
}
}

+ 14
- 0
Teknik/Areas/User/ViewModels/AuthTokenSettingsViewModel.cs 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>();
}
}
}

+ 1
- 1
Teknik/Areas/User/ViewModels/AuthTokenViewModel.cs View File

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

public string Name { get; set; }


Teknik/Areas/User/ViewModels/DeveloperSettingsViewModel.cs → Teknik/Areas/User/ViewModels/ClientSettingsViewModel.cs View File

@@ -5,15 +5,12 @@ using System.Threading.Tasks;

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 DeveloperSettingsViewModel()
public ClientSettingsViewModel()
{
AuthTokens = new List<AuthTokenViewModel>();
Clients = new List<ClientViewModel>();
}
}

+ 76
- 0
Teknik/Areas/User/Views/User/Settings/AuthTokenSettings.cshtml 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>

Teknik/Areas/User/Views/User/Settings/AuthToken.cshtml → Teknik/Areas/User/Views/User/Settings/AuthTokenView.cshtml View File

@@ -2,8 +2,8 @@

<li class="list-group-item" id="authToken_@Model.AuthTokenId">
<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-danger text-danger deleteAuthToken" id="deleteAuthToken_@Model.AuthTokenId" data-authId="@Model.AuthTokenId">Delete</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-authTokenId="@Model.AuthTokenId">Delete</button>
</div>
<h4 class="list-group-item-heading" id="authTokenName_@Model.AuthTokenId">@Model.Name</h4>
@if (Model.LastDateUsed.HasValue)

Teknik/Areas/User/Views/User/Settings/DeveloperSettings.cshtml → Teknik/Areas/User/Views/User/Settings/ClientSettings.cshtml View File

@@ -1,4 +1,4 @@
@model Teknik.Areas.Users.ViewModels.DeveloperSettingsViewModel
@model Teknik.Areas.Users.ViewModels.ClientSettingsViewModel

@using Teknik.Areas.Users.ViewModels

@@ -6,7 +6,7 @@
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>
var createClientURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "CreateClient" })';
@@ -133,4 +133,4 @@
</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>

+ 4
- 2
Teknik/Areas/User/Views/User/Settings/Settings.cshtml View File

@@ -14,7 +14,7 @@
<!--left col-->
<div class="panel panel-default">
<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.AccountSettings")" class="list-group-item @(Model.Page == "Account" ? "active" : string.Empty)">Account</a>
@if (Config.BillingConfig.Enabled)
@@ -29,7 +29,9 @@
</div>
<div class="panel panel-default">
<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>
</div>
</div><!--/col-2-->

Teknik/Content/User/Settings/Developer.css → Teknik/Content/User/Settings/Client.css View File


+ 196
- 0
Teknik/Scripts/User/AuthTokenSettings.js 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);
});
}

Teknik/Scripts/User/DeveloperSettings.js → Teknik/Scripts/User/ClientSettings.js View File


+ 97
- 0
Teknik/Security/AuthTokenAuthenticationHandler.cs 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");
}
}
}

+ 13
- 0
Teknik/Security/AuthTokenSchemeOptions.cs 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
{
}
}

+ 6
- 5
Teknik/Startup.cs View File

@@ -227,33 +227,34 @@ namespace Teknik
};

options.Events.OnRemoteFailure = HandleOnRemoteFailure;
});
})
.AddScheme<AuthTokenSchemeOptions, AuthTokenAuthenticationHandler>("AuthToken", "AuthToken", options => { });

services.AddAuthorization(options =>
{
options.AddPolicy("FullAPI", p =>
{
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.read");
p.RequireScope("teknik-api.write");
});
options.AddPolicy("ReadAPI", p =>
{
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.read");
});
options.AddPolicy("WriteAPI", p =>
{
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.write");
});
options.AddPolicy("AnyAPI", p =>
{
p.AddAuthenticationSchemes("Bearer");
p.AddAuthenticationSchemes("AuthToken");
p.AddAuthenticationSchemes("Bearer");
p.RequireScope("teknik-api.read", "teknik-api.write");
});
});

+ 10
- 4
Teknik/bundleconfig.json View File

@@ -274,15 +274,21 @@
]
},
{
"outputFileName": "./wwwroot/js/user.settings.developer.min.js",
"outputFileName": "./wwwroot/js/user.settings.client.min.js",
"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": [
"./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"
]
},
{

+ 17
- 0
Utilities/Attributes/EntityAttribute.cs 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; }
}
}

+ 2
- 0
Utilities/Constants.cs View File

@@ -16,6 +16,8 @@ namespace Teknik.Utilities

public const string NONCE_KEY = "Nonce";

public const string AUTH_TOKEN_MATCH = "AuthToken";

public const string NullIpAddress = "::1";
}
}

Loading…
Cancel
Save