Browse Source

Added customization of scopes and grant type for clients

master
Teknikode 1 month ago
parent
commit
94be2cf236

+ 34
- 9
IdentityServer/Controllers/ManageController.cs View File

@@ -542,19 +542,15 @@ namespace Teknik.IdentityServer.Controllers
ClientName = model.Name,
ClientUri = model.HomepageUrl,
LogoUri = model.LogoUrl,
AllowedGrantTypes = new List<string>()
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials
},

AllowedGrantTypes = model.AllowedGrants,
AllowedScopes = model.AllowedScopes,

ClientSecrets =
{
new IdentityServer4.Models.Secret(clientSecret.Sha256())
},

RequireConsent = true,

RedirectUris =
{
model.CallbackUrl
@@ -565,8 +561,7 @@ namespace Teknik.IdentityServer.Controllers
origin
},

AllowedScopes = model.AllowedScopes,

RequireConsent = true,
AllowOfflineAccess = true
};

@@ -617,6 +612,36 @@ namespace Teknik.IdentityServer.Controllers
newOrigin.Origin = origin;
configContext.Add(newUri);

// Update their allowed grants
var curGrants = configContext.Set<ClientGrantType>().Where(c => c.ClientId == foundClient.Id).ToList();
if (curGrants != null)
{
configContext.RemoveRange(curGrants);
}
foreach (var grantType in model.AllowedGrants)
{
var newGrant = new ClientGrantType();
newGrant.Client = foundClient;
newGrant.ClientId = foundClient.Id;
newGrant.GrantType = grantType;
configContext.Add(newGrant);
}

// Update their allowed scopes
var curScopes = configContext.Set<ClientScope>().Where(c => c.ClientId == foundClient.Id).ToList();
if (curScopes != null)
{
configContext.RemoveRange(curScopes);
}
foreach (var scope in model.AllowedScopes)
{
var newScope = new ClientScope();
newScope.Client = foundClient;
newScope.ClientId = foundClient.Id;
newScope.Scope = scope;
configContext.Add(newScope);
}

// Save all the changed
configContext.SaveChanges();


+ 1
- 0
IdentityServer/Models/Manage/CreateClientModel.cs View File

@@ -13,5 +13,6 @@ namespace Teknik.IdentityServer.Models.Manage
public string LogoUrl { get; set; }
public string CallbackUrl { get; set; }
public ICollection<string> AllowedScopes { get; set; }
public ICollection<string> AllowedGrants { get; set; }
}
}

+ 2
- 0
IdentityServer/Models/Manage/EditClientModel.cs View File

@@ -13,5 +13,7 @@ namespace Teknik.IdentityServer.Models.Manage
public string HomepageUrl { get; set; }
public string LogoUrl { get; set; }
public string CallbackUrl { get; set; }
public ICollection<string> AllowedScopes { get; set; }
public ICollection<string> AllowedGrants { get; set; }
}
}

+ 2
- 2
IdentityServer/Properties/launchSettings.json View File

@@ -21,7 +21,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:6002/"
"applicationUrl": "https://localhost:9002/"
},
"IdentityServer - Prod": {
"commandName": "Project",
@@ -29,7 +29,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
},
"applicationUrl": "https://localhost:6002/"
"applicationUrl": "https://localhost:9002/"
}
}
}

+ 0
- 1
IdentityServer/Views/Account/LoginWith2fa.cshtml View File

@@ -18,7 +18,6 @@
<div class="form-group">
<label asp-for="TwoFactorCode"></label>
<input asp-for="TwoFactorCode" class="form-control" autocomplete="off" />
<span asp-validation-for="TwoFactorCode" class="text-danger"></span>
</div>
<div class="form-group">
<div class="abc-checkbox">

+ 0
- 1
IdentityServer/Views/Account/LoginWithRecoveryCode.cshtml View File

@@ -20,7 +20,6 @@
<div class="form-group">
<label asp-for="RecoveryCode"></label>
<input asp-for="RecoveryCode" class="form-control" autocomplete="off" />
<span asp-validation-for="RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default pull-right">Log in</button>
</form>

+ 30
- 7
Teknik/Areas/User/Controllers/UserController.cs View File

@@ -437,7 +437,8 @@ namespace Teknik.Areas.Users.Controllers
HomepageUrl = client.ClientUri,
LogoUrl = client.LogoUri,
CallbackUrl = string.Join(',', client.RedirectUris),
AllowedScopes = client.AllowedScopes
AllowedScopes = client.AllowedScopes,
GrantType = IdentityHelper.GrantsToGrantType(client.AllowedGrantTypes.ToArray())
});
}

@@ -1239,7 +1240,7 @@ namespace Teknik.Areas.Users.Controllers

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateClient(string name, string homepageUrl, string logoUrl, string callbackUrl, [FromServices] ICompositeViewEngine viewEngine)
public async Task<IActionResult> CreateClient(string name, string homepageUrl, string logoUrl, string callbackUrl, IdentityClientGrant grantType, string scopes, [FromServices] ICompositeViewEngine viewEngine)
{
try
{
@@ -1255,7 +1256,15 @@ namespace Teknik.Areas.Users.Controllers
return Json(new { error = "Invalid logo URL" });

// Validate the code with the identity server
var result = await IdentityHelper.CreateClient(_config, User.Identity.Name, name, homepageUrl, logoUrl, callbackUrl, "openid", "role", "account-info", "security-info", "teknik-api.read", "teknik-api.write");
var result = await IdentityHelper.CreateClient(
_config,
User.Identity.Name,
name,
homepageUrl,
logoUrl,
callbackUrl,
IdentityHelper.GrantTypeToGrants(grantType),
scopes.Split(','));

if (result.Success)
{
@@ -1267,6 +1276,8 @@ namespace Teknik.Areas.Users.Controllers
model.HomepageUrl = homepageUrl;
model.LogoUrl = logoUrl;
model.CallbackUrl = callbackUrl;
model.GrantType = grantType;
model.AllowedScopes = scopes.Split(',');

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

@@ -1287,14 +1298,15 @@ namespace Teknik.Areas.Users.Controllers
Client foundClient = await IdentityHelper.GetClient(_config, User.Identity.Name, clientId);
if (foundClient != null)
{
ClientViewModel model = new ClientViewModel()
ClientModifyViewModel model = new ClientModifyViewModel()
{
Id = foundClient.ClientId,
Name = foundClient.ClientName,
HomepageUrl = foundClient.ClientUri,
LogoUrl = foundClient.LogoUri,
CallbackUrl = string.Join(',', foundClient.RedirectUris),
AllowedScopes = foundClient.AllowedScopes
AllowedScopes = foundClient.AllowedScopes,
GrantType = IdentityHelper.GrantsToGrantType(foundClient.AllowedGrantTypes.ToArray()).ToString()
};

return Json(new { result = true, client = model });
@@ -1304,7 +1316,7 @@ namespace Teknik.Areas.Users.Controllers

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditClient(string clientId, string name, string homepageUrl, string logoUrl, string callbackUrl, [FromServices] ICompositeViewEngine viewEngine)
public async Task<IActionResult> EditClient(string clientId, string name, string homepageUrl, string logoUrl, string callbackUrl, IdentityClientGrant grantType, string scopes, [FromServices] ICompositeViewEngine viewEngine)
{
try
{
@@ -1325,7 +1337,16 @@ namespace Teknik.Areas.Users.Controllers
return Json(new { error = "Client does not exist" });

// Validate the code with the identity server
var result = await IdentityHelper.EditClient(_config, User.Identity.Name, clientId, name, homepageUrl, logoUrl, callbackUrl);
var result = await IdentityHelper.EditClient(
_config,
User.Identity.Name,
clientId,
name,
homepageUrl,
logoUrl,
callbackUrl,
IdentityHelper.GrantTypeToGrants(grantType),
scopes.Split(','));

if (result.Success)
{
@@ -1337,6 +1358,8 @@ namespace Teknik.Areas.Users.Controllers
model.HomepageUrl = homepageUrl;
model.LogoUrl = logoUrl;
model.CallbackUrl = callbackUrl;
model.GrantType = grantType;
model.AllowedScopes = scopes.Split(',');

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


+ 48
- 4
Teknik/Areas/User/Utility/IdentityHelper.cs View File

@@ -71,6 +71,47 @@ namespace Teknik.Areas.Users.Utility
return new IdentityResult() { Success = false, Message = "HTTP Error: " + response.StatusCode + " | " + (await response.Content.ReadAsStringAsync()) };
}

public static string[] GrantTypeToGrants(IdentityClientGrant grantType)
{
List<string> grants = new List<string>();
switch (grantType)
{
case IdentityClientGrant.Implicit:
grants.Add(GrantType.Implicit);
break;
case IdentityClientGrant.AuthorizationCode:
grants.Add(GrantType.AuthorizationCode);
break;
case IdentityClientGrant.ClientCredentials:
grants.Add(GrantType.ClientCredentials);
break;
default:
grants.Add(GrantType.Hybrid);
break;
}
return grants.ToArray();
}

public static IdentityClientGrant GrantsToGrantType(string[] grants)
{
if (grants.Contains(GrantType.Implicit))
{
return IdentityClientGrant.Implicit;
}
else if (grants.Contains(GrantType.AuthorizationCode))
{
return IdentityClientGrant.AuthorizationCode;
}
else if (grants.Contains(GrantType.ClientCredentials))
{
return IdentityClientGrant.ClientCredentials;
}
else
{
return IdentityClientGrant.ClientCredentials;
}
}

// API Functions

public static async Task<IdentityResult> CreateUser(Config config, string username, string password, string recoveryEmail)
@@ -350,7 +391,7 @@ namespace Teknik.Areas.Users.Utility
throw new Exception(result.Message);
}

public static async Task<IdentityResult> CreateClient(Config config, string username, string name, string homepageUrl, string logoUrl, string callbackUrl, params string[] allowedScopes)
public static async Task<IdentityResult> CreateClient(Config config, string username, string name, string homepageUrl, string logoUrl, string callbackUrl, string[] allowedGrants, string[] allowedScopes)
{
var manageUrl = CreateUrl(config, $"Manage/CreateClient");

@@ -362,12 +403,13 @@ namespace Teknik.Areas.Users.Utility
homepageUrl = homepageUrl,
logoUrl = logoUrl,
callbackUrl = callbackUrl,
allowedScopes = allowedScopes
allowedScopes = allowedScopes,
allowedGrants = allowedGrants
});
return response;
}

public static async Task<IdentityResult> EditClient(Config config, string username, string clientId, string name, string homepageUrl, string logoUrl, string callbackUrl)
public static async Task<IdentityResult> EditClient(Config config, string username, string clientId, string name, string homepageUrl, string logoUrl, string callbackUrl, string[] allowedGrants, string[] allowedScopes)
{
var manageUrl = CreateUrl(config, $"Manage/EditClient");

@@ -379,7 +421,9 @@ namespace Teknik.Areas.Users.Utility
name = name,
homepageUrl = homepageUrl,
logoUrl = logoUrl,
callbackUrl = callbackUrl
callbackUrl = callbackUrl,
allowedScopes = allowedScopes,
allowedGrants = allowedGrants
});
return response;
}

+ 20
- 0
Teknik/Areas/User/ViewModels/ClientModifyViewModel.cs View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Utilities;
using Teknik.ViewModels;

namespace Teknik.Areas.Users.ViewModels
{
public class ClientModifyViewModel : ViewModelBase
{
public string Id { get; set; }
public string Name { get; set; }
public string HomepageUrl { get; set; }
public string LogoUrl { get; set; }
public string CallbackUrl { get; set; }
public ICollection<string> AllowedScopes { get; set; }
public string GrantType { get; set; }
}
}

+ 2
- 0
Teknik/Areas/User/ViewModels/ClientViewModel.cs View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Teknik.Utilities;
using Teknik.ViewModels;

namespace Teknik.Areas.Users.ViewModels
@@ -14,5 +15,6 @@ namespace Teknik.Areas.Users.ViewModels
public string LogoUrl { get; set; }
public string CallbackUrl { get; set; }
public ICollection<string> AllowedScopes { get; set; }
public IdentityClientGrant GrantType { get; set; }
}
}

+ 28
- 1
Teknik/Areas/User/Views/User/Settings/ClientView.cshtml View File

@@ -27,6 +27,33 @@
</div>
</div>
</div>
<div class="col-sm-4">
<div class="row">
<div class="col-sm-12">
<strong>Allowed Scopes</strong>
</div>
</div>
<div class="row">
<div class="col-sm-12">
@(string.Join(", ", Model.AllowedScopes))
</div>
</div>
</div>
<div class="col-sm-4">
<div class="row">
<div class="col-sm-12">
<strong>Grant Type</strong>
</div>
</div>
<div class="row">
<div class="col-sm-12">
@Model.GrantType.GetDescription()
</div>
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-sm-4">
<div class="row">
<div class="col-sm-12">
@@ -39,7 +66,7 @@
</div>
</div>
</div>
<div class="col-sm-4">
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12">
<strong>Authorization Callback Url</strong>

+ 43
- 0
Teknik/Areas/User/Views/User/Settings/DeveloperSettings.cshtml View File

@@ -6,6 +6,8 @@
Layout = "~/Areas/User/Views/User/Settings/Settings.cshtml";
}

<bundle src="css/user.settings.developer.min.css" append-version="true"></bundle>

<script>
var createClientURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "CreateClient" })';
var editClientURL = '@Url.SubRouteUrl("account", "User.Action", new { action = "EditClient" })';
@@ -79,6 +81,47 @@
<label for="clientCallbackUrl">Authorization callback URL <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="clientCallbackUrl" name="clientCallbackUrl" data-val-required="The Authorization callback URL field is required." data-val="true" />
</div>
<div class="form-group" id="grantSection">
<label for="grantType">Grant Type</label>
<select class="form-control" name="grantType" id="grantType">
@foreach (IdentityClientGrant grant in Enum.GetValues(typeof(IdentityClientGrant)))
{
<!option value="@grant">@grant.GetDescription()</!option>
}
</select>
</div>
<div class="form-group" id="scopeSection">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-tasks"></span>
Allowed Scopes
</div>
<ul class="list-group">
@foreach (IdentityClientScope scope in Enum.GetValues(typeof(IdentityClientScope)))
{

<li class="list-group-item abc-checkbox">
<input class="consent-scopecheck @(scope.IsReadOnly() ? "default" : string.Empty)"
type="checkbox"
name="allowedScope"
id="scopes_@scope.GetDisplayName()"
value="@scope.GetDisplayName()"
checked="@scope.IsReadOnly()"
disabled="@scope.IsReadOnly()" />
<label for="ScopesConsented">
<strong>@scope.GetDisplayName()</strong>
</label>
@if (!string.IsNullOrEmpty(scope.GetDescription()))
{
<div class="consent-description">
<label for="scopes_@scope.GetDisplayName()">@scope.GetDescription()</label>
</div>
}
</li>
}
</ul>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<button class="btn btn-primary pull-right hidden clientSubmit" id="clientCreateSubmit" type="submit" name="clientCreateSubmit">Create Client</button>

+ 12
- 0
Teknik/Content/User/Settings/Developer.css View File

@@ -0,0 +1,12 @@
.consent-scopecheck {
display: inline-block;
margin-right: 5px;
}

.consent-description {
margin-left: 25px;
}

.consent-description label {
font-weight: normal;
}

+ 18
- 2
Teknik/Scripts/User/DeveloperSettings.js View File

@@ -25,6 +25,11 @@ $(document).ready(function () {
$("#createClient").click(function () {
$('#clientModal').find('#clientCreateSubmit').removeClass('hidden');
$('#clientModal').find('#clientCreateSubmit').text('Create Client');
_.forEach($('#clientModal').find('#scopeSection :checkbox'), function (cb) {
if ($(cb).hasClass('default')) {
$(cb).prop('checked', true);
}
});

$('#clientModal').modal('show');
});
@@ -101,6 +106,11 @@ function editClient(clientId) {
$('#clientModal').find('#clientHomepageUrl').val(data.client.homepageUrl);
$('#clientModal').find('#clientLogoUrl').val(data.client.logoUrl);
$('#clientModal').find('#clientCallbackUrl').val(data.client.callbackUrl);
$('#clientModal').find('#grantType').val(data.client.grantType);

_.forEach(data.client.allowedScopes, function (scope) {
$('#clientModal').find('#scopes_' + scope).prop('checked', true);
});

$('#clientModal').find('#clientEditSubmit').removeClass('hidden');
$('#clientModal').find('#clientEditSubmit').text('Save Client');
@@ -168,7 +178,7 @@ function deleteClient(clientId) {
}

function saveClientInfo(url, submitText, submitActionText, callback) {
var clientId, name, homepageUrl, logoUrl, callbackUrl;
var clientId, name, homepageUrl, logoUrl, callbackUrl, grantType, scopes;
disableButton('.clientSubmit', submitActionText);

clientId = $('#clientModal').find('#clientId').val();
@@ -176,11 +186,17 @@ function saveClientInfo(url, submitText, submitActionText, callback) {
homepageUrl = $('#clientModal').find('#clientHomepageUrl').val();
logoUrl = $('#clientModal').find('#clientLogoUrl').val();
callbackUrl = $('#clientModal').find('#clientCallbackUrl').val();
grantType = $('#clientModal').find('#grantType').val();
scopes = $('#clientModal').find('#scopeSection :checkbox:checked');
scopes = _.map(scopes, function (cb) {
return cb.value;
});
scopes = scopes.join(',');

$.ajax({
type: "POST",
url: url,
data: AddAntiForgeryToken({ clientId: clientId, name: name, homepageUrl: homepageUrl, logoUrl: logoUrl, callbackUrl: callbackUrl }),
data: AddAntiForgeryToken({ clientId: clientId, name: name, homepageUrl: homepageUrl, logoUrl: logoUrl, callbackUrl: callbackUrl, grantType: grantType, scopes: scopes }),
success: function (response) {
if (response.result) {
if (callback) {

+ 3
- 0
Teknik/Scripts/common.js View File

@@ -155,6 +155,9 @@ function clearInputs(parent) {
$(parent).find('textarea').each(function () {
$(this).val('');
});
$(parent).find('input:checkbox').each(function () {
$(this).prop('checked', false);
});
}

String.prototype.hashCode = function () {

+ 6
- 0
Teknik/bundleconfig.json View File

@@ -263,6 +263,12 @@
"./wwwroot/js/app/User/DeveloperSettings.js"
]
},
{
"outputFileName": "./wwwroot/css/user.settings.developer.min.css",
"inputFiles": [
"./wwwroot/css/app/User/Settings/Developer.css"
]
},
{
"outputFileName": "./wwwroot/js/user.settings.upload.min.js",
"inputFiles": [

+ 54
- 0
Utilities/Extensions.cs View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Teknik.Utilities
{
public static class Extensions
{
public static string GetDescription<T>(this T value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());

DescriptionAttribute[] attributes = fi.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];

if (attributes != null && attributes.Any())
{
return attributes.First().Description;
}

return value.ToString();
}

public static string GetDisplayName<T>(this T value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());

DisplayNameAttribute[] attributes = fi.GetCustomAttributes(typeof(DisplayNameAttribute), false) as DisplayNameAttribute[];

if (attributes != null && attributes.Any())
{
return attributes.First().DisplayName;
}

return value.ToString();
}

public static bool IsReadOnly<T>(this T value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());

ReadOnlyAttribute[] attributes = fi.GetCustomAttributes(typeof(ReadOnlyAttribute), false) as ReadOnlyAttribute[];

if (attributes != null && attributes.Any())
{
return attributes.First().IsReadOnly;
}

return false;
}
}
}

+ 17
- 0
Utilities/IdentityClientGrant.cs View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;

namespace Teknik.Utilities
{
public enum IdentityClientGrant
{
[Description("Authorization Code")]
AuthorizationCode,
[Description("Client Credentials")]
ClientCredentials,
[Description("Implicit")]
Implicit,
}
}

+ 30
- 0
Utilities/IdentityClientScope.cs View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;

namespace Teknik.Utilities
{
public enum IdentityClientScope
{
[DisplayName("openid")]
[Description("The user identifier")]
[ReadOnly(true)]
openid,
[DisplayName("role")]
[Description("The user role")]
role,
[DisplayName("account-info")]
[Description("A user's account information")]
accountInfo,
[DisplayName("security-info")]
[Description("A user's security information")]
securityInfo,
[DisplayName("teknik-api.read")]
[Description("Read access to the Teknik API")]
teknikApiRead,
[DisplayName("teknik-api.write")]
[Description("Write access to the Teknik API")]
teknikApiWrite,
}
}

Loading…
Cancel
Save