Merge pull request #217 from aspnetzero/identity-server

Identity server
pull/221/head
Halil İbrahim Kalkan 7 years ago committed by GitHub
commit 13ad335cb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -257,4 +257,5 @@ src/AbpDesk/AbpDesk.Web.Mvc/Logs
/src/Volo.Abp.Identity.HttpApi.Host/Logs
src/MicroserviceDemo/MicroserviceDemo.Web/Logs/*.txt
src/MicroserviceDemo/MicroserviceDemo.TenantService/Logs/*.txt
src/MicroserviceDemo/MicroserviceDemo.AuthServer/Logs/*.txt
src/MicroserviceDemo/MicroserviceDemo.AuthServer/Logs/*.txt
src/MicroserviceDemo/MicroserviceDemo.TenancyService/Logs/*.txt

@ -296,9 +296,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroserviceDemo.TenancySer
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiTenancy.HttpApi.Client", "src\Volo.Abp.MultiTenancy.HttpApi.Client\Volo.Abp.MultiTenancy.HttpApi.Client.csproj", "{76D24E2C-8DB0-48B7-9FC4-02231B8B9F39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroserviceDemo.AuthServer", "src\MicroserviceDemo\MicroserviceDemo.AuthServer\MicroserviceDemo.AuthServer.csproj", "{A177B8B7-ACAD-4F75-B6D4-B72F7BC2E40A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroserviceDemo.AuthServer", "src\MicroserviceDemo\MicroserviceDemo.AuthServer\MicroserviceDemo.AuthServer.csproj", "{A177B8B7-ACAD-4F75-B6D4-B72F7BC2E40A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Account.Web.IdentityServer", "src\Volo.Abp.Account.Web.IdentityServer\Volo.Abp.Account.Web.IdentityServer.csproj", "{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Account.Web.IdentityServer", "src\Volo.Abp.Account.Web.IdentityServer\Volo.Abp.Account.Web.IdentityServer.csproj", "{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroserviceDemo.ConsoleClient", "src\MicroserviceDemo\MicroserviceDemo.ConsoleClient\MicroserviceDemo.ConsoleClient.csproj", "{CD4E755D-D47C-45B1-AFB3-3444FF2E2E39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Authentication.OAuth", "src\Volo.Abp.AspNetCore.Authentication.OAuth\Volo.Abp.AspNetCore.Authentication.OAuth.csproj", "{A1C792B7-0DBF-460D-9158-A1A68A2D9C1A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Authentication.OAuth.Tests", "test\Volo.Abp.AspNetCore.Authentication.OAuth.Tests\Volo.Abp.AspNetCore.Authentication.OAuth.Tests.csproj", "{627B88DB-BDCF-4D92-8454-EFE95F4AFB7A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -790,6 +796,18 @@ Global
{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}.Release|Any CPU.Build.0 = Release|Any CPU
{CD4E755D-D47C-45B1-AFB3-3444FF2E2E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD4E755D-D47C-45B1-AFB3-3444FF2E2E39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD4E755D-D47C-45B1-AFB3-3444FF2E2E39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD4E755D-D47C-45B1-AFB3-3444FF2E2E39}.Release|Any CPU.Build.0 = Release|Any CPU
{A1C792B7-0DBF-460D-9158-A1A68A2D9C1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1C792B7-0DBF-460D-9158-A1A68A2D9C1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1C792B7-0DBF-460D-9158-A1A68A2D9C1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1C792B7-0DBF-460D-9158-A1A68A2D9C1A}.Release|Any CPU.Build.0 = Release|Any CPU
{627B88DB-BDCF-4D92-8454-EFE95F4AFB7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{627B88DB-BDCF-4D92-8454-EFE95F4AFB7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{627B88DB-BDCF-4D92-8454-EFE95F4AFB7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{627B88DB-BDCF-4D92-8454-EFE95F4AFB7A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -936,6 +954,9 @@ Global
{76D24E2C-8DB0-48B7-9FC4-02231B8B9F39} = {CA154803-3589-47B3-B7CB-B18F94FE1EB6}
{A177B8B7-ACAD-4F75-B6D4-B72F7BC2E40A} = {3510E248-DC9F-4A07-8134-02E7F5CC5783}
{E4AB8A4F-BB59-4BDB-B915-877CE97D8113} = {DB012309-74FD-4D5A-B843-DD77BF053BF4}
{CD4E755D-D47C-45B1-AFB3-3444FF2E2E39} = {3510E248-DC9F-4A07-8134-02E7F5CC5783}
{A1C792B7-0DBF-460D-9158-A1A68A2D9C1A} = {C4C6961D-01CC-49A5-8B96-2A36E71CF01F}
{627B88DB-BDCF-4D92-8454-EFE95F4AFB7A} = {37087D1B-3693-4E96-983D-A69F210BDE53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

@ -61,7 +61,6 @@ namespace AbpDesk.Web.Mvc
services.PreConfigure<IMvcBuilder>(builder =>
{
builder
.AddViewLocalization() //TODO: Move to the framework!
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/App");

@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.0" />
</ItemGroup>

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Builder;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -6,12 +7,15 @@ using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Modularity;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap;
using Volo.Abp.Autofac;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Identity.EntityFrameworkCore;
using Volo.Abp.IdentityServer.EntityFrameworkCore;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace MicroserviceDemo.AuthServer
{
@ -43,6 +47,33 @@ namespace MicroserviceDemo.AuthServer
});
});
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
if (hostingEnvironment.IsDevelopment())
{
services.Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.ReplaceEmbeddedByPyhsical<AbpAspNetCoreMvcUiModule>(Path.Combine(hostingEnvironment.ContentRootPath, "..\\..\\Volo.Abp.AspNetCore.Mvc.UI"));
options.FileSets.ReplaceEmbeddedByPyhsical<AbpAspNetCoreMvcUiBootstrapModule>(Path.Combine(hostingEnvironment.ContentRootPath, "..\\..\\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap"));
options.FileSets.ReplaceEmbeddedByPyhsical<AbpAccountWebModule>(Path.Combine(hostingEnvironment.ContentRootPath, "..\\..\\Volo.Abp.Account.Web"));
options.FileSets.ReplaceEmbeddedByPyhsical<AbpAccountWebIdentityServerModule>(Path.Combine(hostingEnvironment.ContentRootPath, "..\\..\\Volo.Abp.Account.Web.IdentityServer"));
});
}
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = configuration["Authentication:Facebook:AppId"];
options.AppSecret = configuration["Authentication:Facebook:AppSecret"];
options.Scope.Add("email");
options.Scope.Add("public_profile");
});
services.AddAssemblyOf<MicroservicesAuthServerModule>();
}
@ -58,7 +89,19 @@ namespace MicroserviceDemo.AuthServer
app.UseStaticFiles();
app.UseVirtualFiles();
app.UseMvcWithDefaultRoute();
app.UseIdentityServer(); //This internally adds .UseAuthentication() (we should be carefully about that)
app.UseMvc(routes =>
{
//TODO: Can we make an extension method for adding these two routes inside the framework?
routes.MapRoute(
name: "defaultWithArea",
template: "{area}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
private static IConfigurationRoot BuildConfiguration(IHostingEnvironment env)

@ -0,0 +1,20 @@
@page
@model MicroserviceDemo.AuthServer.Pages.IndexModel
<h3>Welcome to the Authentication Server!</h3>
<abp-card>
<abp-card-header></abp-card-header>
<abp-card-body>
<a href="~/Account/Login">Login</a>
@if (User.Identity.IsAuthenticated)
{
foreach (var claim in User.Claims)
{
<div>@claim.Type = @claim.Value</div>
}
}
</abp-card-body>
</abp-card>

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MicroserviceDemo.AuthServer.Pages
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}

@ -1,5 +1,12 @@
{
"ConnectionStrings": {
"Default": "Server=localhost;Database=MicroservicesDemo.Web;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Authentication": {
"Facebook": {
"IsEnabled": "true",
"AppId": "911417875702990",
"AppSecret": "adea0bff222ae340d8fb0ce3e6275d6b"
}
}
}

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityModel" Version="3.1.0" />
</ItemGroup>
</Project>

@ -0,0 +1,54 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
namespace MicroserviceDemo.ConsoleClient
{
class Program
{
static void Main(string[] args)
{
RunDemo().Wait();
Console.ReadLine();
}
private static async Task RunDemo()
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:54307");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("multi-tenancy-api");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
// call api
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:63877/api/multi-tenancy/tenant?SkipCount=0&MaxResultCount=100");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
}
}
}

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.AspNetCore.Mvc;
namespace MicroserviceDemo.Web.Controllers
{
public class AccountController : AbpController
{
public async Task Logout()
{
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
await HttpContext.SignOutAsync("oidc");
}
}
}

@ -25,9 +25,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Volo.Abp.Account.Web\Volo.Abp.Account.Web.csproj" />
<ProjectReference Include="..\..\Volo.Abp.AspNetCore.Authentication.OAuth\Volo.Abp.AspNetCore.Authentication.OAuth.csproj" />
<ProjectReference Include="..\..\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" />
<ProjectReference Include="..\..\Volo.Abp.Identity.EntityFrameworkCore\Volo.Abp.Identity.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\Volo.Abp.Identity.HttpApi\Volo.Abp.Identity.HttpApi.csproj" />
<ProjectReference Include="..\..\Volo.Abp.Identity.Web\Volo.Abp.Identity.Web.csproj" />

@ -1,12 +1,15 @@
using System;
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Swashbuckle.AspNetCore.Swagger;
using Volo.Abp;
using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Authentication.OAuth;
using Volo.Abp.AspNetCore.Modularity;
using Volo.Abp.AspNetCore.Mvc.Bundling;
using Volo.Abp.Autofac;
@ -19,30 +22,20 @@ using Volo.Abp.Identity.Web;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.MultiTenancy.Web;
using Volo.Abp.Permissions;
using Volo.Abp.Permissions.EntityFrameworkCore;
namespace MicroserviceDemo.Web
{
[DependsOn(typeof(AbpAutofacModule))]
[DependsOn(typeof(AbpHttpClientModule))]
[DependsOn(typeof(AbpPermissionsEntityFrameworkCoreModule))]
[DependsOn(typeof(AbpIdentityHttpApiModule))]
[DependsOn(typeof(AbpIdentityWebModule))]
[DependsOn(typeof(AbpIdentityEntityFrameworkCoreModule))]
[DependsOn(typeof(AbpAccountWebModule))]
[DependsOn(typeof(AbpMultiTenancyHttpApiClientModule))]
[DependsOn(typeof(AbpMultiTenancyWebModule))]
[DependsOn(typeof(AbpAspNetCoreAuthenticationOAuthModule))]
public class MicroservicesDemoWebModule : AbpModule
{
public override void PreConfigureServices(IServiceCollection services)
{
services.PreConfigure<IMvcBuilder>(builder =>
{
builder.AddViewLocalization(); //TODO: To the framework!
});
}
public override void ConfigureServices(IServiceCollection services)
{
var hostingEnvironment = services.GetSingletonInstance<IHostingEnvironment>();
@ -74,8 +67,34 @@ namespace MicroserviceDemo.Web
"/Abp/ServiceProxyScript?_v=" + DateTime.Now.Ticks
});
});
services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = IdentityConstants.ApplicationScheme;
options.Authority = "http://localhost:54307";
options.RequireHttpsMetadata = false;
services.AddAuthentication();
options.ClientId = "client";
options.ClientSecret = "secret";
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("role");
options.Scope.Add("email");
options.Scope.Add("phone");
options.Scope.Add("multi-tenancy-api");
options.ClaimActions.MapAbpClaimTypes();
});
services.Configure<RemoteServiceOptions>(configuration);
@ -109,7 +128,16 @@ namespace MicroserviceDemo.Web
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "defaultWithArea",
template: "{area}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
private static IConfigurationRoot BuildConfiguration(IHostingEnvironment env)

@ -1,11 +1,14 @@
@page
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Http
@model MicroserviceDemo.Web.Pages.IndexModel
@inject IHttpContextAccessor HttpContextAccessor
<p>
<abp-card>
<abp-card-header></abp-card-header>
<abp-card-body>
<a href="~/Account/Login">Login</a>
@if (User.Identity.IsAuthenticated)
{
foreach (var claim in User.Claims)
@ -14,6 +17,18 @@
}
}
<hr />
@{
var accessToken = await HttpContextAccessor.HttpContext.GetTokenAsync("access_token");
var refreshToken = await HttpContextAccessor.HttpContext.GetTokenAsync("refresh_token");
}
accessToken = @accessToken <br />
refreshToken = @refreshToken <br />
isInRole(admin) = @User.IsInRole("admin")
</abp-card-body>
</abp-card>
</p>

@ -1,20 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.IdentityServer;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Account.Web
{
[DependsOn(typeof(AbpAccountWebModule))]
[DependsOn(typeof(AbpIdentityServerDomainModule))]
public class AbpAccountWebIdentityServerModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpAccountWebModule>();
services.Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpAccountWebModule>("Volo.Abp.Account.Web");
options.FileSets.AddEmbedded<AbpAccountWebIdentityServerModule>("Volo.Abp.Account.Web");
});
services.AddAssemblyOf<AbpAccountWebIdentityServerModule>();
}
}
}

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Identity;
namespace Volo.Abp.Account.Web.Auth
{
//TODO: Move to the framework..?
public static class SignInResultExtensions
{
public static string GetResultAsString(this SignInResult signInResult)
{
if (signInResult.Succeeded)
{
return "Succeeded";
}
if (signInResult.IsLockedOut)
{
return "IsLockedOut";
}
if (signInResult.IsNotAllowed)
{
return "IsNotAllowed";
}
if (signInResult.RequiresTwoFactor)
{
return "RequiresTwoFactor";
}
return "Unknown";
}
}
}

@ -0,0 +1,82 @@
@page
@using Volo.Abp.Account.Web.Pages.Account
@model GrantsModel
@* TODO: Should work on the page HTML, copy/pasted from IDS4 samples *@
<div class="grants">
<div class="row page-header">
<div class="col-sm-10">
<h1>
Client Application Access
</h1>
<div>Below is the list of applications you have given access to and the names of the resources they have access to.</div>
</div>
</div>
@if (Model.Grants.Any() == false)
{
<div class="row">
<div class="col-sm-8">
<div class="alert alert-info">
You have not given access to any applications
</div>
</div>
</div>
}
else
{
foreach (var grant in Model.Grants)
{
<div class="row grant">
<div class="col-sm-2">
@if (grant.ClientLogoUrl != null)
{
<img src="@grant.ClientLogoUrl">
}
</div>
<div class="col-sm-8">
<div class="clientname">@grant.ClientName</div>
<div>
<span class="created">Created:</span> @grant.Created.ToString("yyyy-MM-dd")
</div>
@if (grant.Expires.HasValue)
{
<div>
<span class="expires">Expires:</span> @grant.Expires.Value.ToString("yyyy-MM-dd")
</div>
}
@if (grant.IdentityGrantNames.Any())
{
<div>
<div class="granttype">Identity Grants</div>
<ul>
@foreach (var name in grant.IdentityGrantNames)
{
<li>@name</li>
}
</ul>
</div>
}
@if (grant.ApiGrantNames.Any())
{
<div>
<div class="granttype">API Grants</div>
<ul>
@foreach (var name in grant.ApiGrantNames)
{
<li>@name</li>
}
</ul>
</div>
}
</div>
<div class="col-sm-2">
<form asp-page="./Grants" asp-page-handler="Revoke" method="post">
<input type="hidden" name="clientId" value="@grant.ClientId">
<button class="btn btn-danger">Revoke Access</button>
</form>
</div>
</div>
}
}
</div>

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.RazorPages;
namespace Volo.Abp.Account.Web.Pages.Account
{
public class GrantsModel : AbpPageModel
{
public List<GrantViewModel> Grants { get; set; }
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clients;
private readonly IResourceStore _resources;
public GrantsModel(IIdentityServerInteractionService interaction,
IClientStore clients,
IResourceStore resources)
{
_interaction = interaction;
_clients = clients;
_resources = resources;
}
public async Task OnGet()
{
Grants = new List<GrantViewModel>();
foreach (var consent in await _interaction.GetAllUserConsentsAsync())
{
var client = await _clients.FindClientByIdAsync(consent.ClientId);
if (client != null)
{
var resources = await _resources.FindResourcesByScopeAsync(consent.Scopes);
var item = new GrantViewModel
{
ClientId = client.ClientId,
ClientName = client.ClientName ?? client.ClientId,
ClientLogoUrl = client.LogoUri,
ClientUrl = client.ClientUri,
Created = consent.CreationTime,
Expires = consent.Expiration,
IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(),
ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray()
};
Grants.Add(item);
}
}
}
public async Task<IActionResult> OnPostRevokeAsync(string clientId)
{
await _interaction.RevokeUserConsentAsync(clientId);
return Redirect("/"); //TODO: ..?
}
public class GrantViewModel
{
public string ClientId { get; set; }
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public DateTime Created { get; set; }
public DateTime? Expires { get; set; }
public IEnumerable<string> IdentityGrantNames { get; set; }
public IEnumerable<string> ApiGrantNames { get; set; }
}
}
}

@ -0,0 +1,61 @@
@page
@model Volo.Abp.Account.Web.Pages.Account.IdsLoginModel
<div class="row">
@if (Model.EnableLocalLogin)
{
<div class="col-md-6">
<div class="col-md-4">
<form method="post" asp-page="/Account/Login">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<div class="form-group">
<label asp-for="LoginInput.UserNameOrEmailAddress"></label>
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" />
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LoginInput.Password"></label>
<input asp-for="LoginInput.Password" class="form-control" />
<span asp-validation-for="LoginInput.Password" class="text-danger"></span>
</div>
<div class="form-check">
<label asp-for="LoginInput.RememberMe" class="form-check-label">
<input asp-for="LoginInput.RememberMe" class="form-check-input" />
@Html.DisplayNameFor(m => m.LoginInput.RememberMe)
</label>
</div>
<button type="submit" class="btn btn-primary" name="Action" value="Login">Login</button>
<button type="button" class="btn btn-secondary" name="Action" value="Cancel">Cancel</button> @* TODO: Only show if identity server is used *@
</form>
<div style="padding-top: 20px">
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})">Register</a>
</div>
</div>
</div>
}
@if (Model.VisibleExternalProviders.Any())
{
<div class="col-md-6">
<h4>Use another service to log in.</h4>
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
@foreach (var provider in Model.VisibleExternalProviders)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</form>
</div>
}
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>Invalid login request</strong>
There are no login schemes configured for this client.
</div>
}
</div>

@ -0,0 +1,315 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Events;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Account.Web.Auth;
using Volo.Abp.Identity;
using Volo.Abp.Ui;
using Volo.Abp.Uow;
namespace Volo.Abp.Account.Web.Pages.Account
{
//TODO: Inherit from LoginModel of Account.Web project. We should design it as extensible.
public class IdsLoginModel : AccountModelBase
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; }
[BindProperty]
public LoginInputModel LoginInput { get; set; }
public bool EnableLocalLogin { get; set; }
//TODO: Why there is an ExternalProviders if only the VisibleExternalProviders is used.
public IEnumerable<ExternalProviderModel> ExternalProviders { get; set; }
public IEnumerable<ExternalProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));
public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IdentityUserManager _userManager;
private readonly IIdentityServerInteractionService _interaction;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly AbpAccountOptions _accountOptions;
private readonly IClientStore _clientStore;
private readonly IEventService _identityServerEvents;
public IdsLoginModel(
SignInManager<IdentityUser> signInManager,
IdentityUserManager userManager,
IIdentityServerInteractionService interaction,
IAuthenticationSchemeProvider schemeProvider,
IOptions<AbpAccountOptions> accountOptions,
IClientStore clientStore,
IEventService identityServerEvents)
{
_signInManager = signInManager;
_userManager = userManager;
_interaction = interaction;
_schemeProvider = schemeProvider;
_clientStore = clientStore;
_identityServerEvents = identityServerEvents;
_accountOptions = accountOptions.Value;
}
public async Task OnGetAsync()
{
LoginInput = new LoginInputModel();
var context = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
LoginInput.UserNameOrEmailAddress = context?.LoginHint;
if (context?.IdP != null)
{
LoginInput.UserNameOrEmailAddress = context.LoginHint;
ExternalProviders = new[] {new ExternalProviderModel {AuthenticationScheme = context.IdP}};
return;
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null || x.Name.Equals(_accountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
EnableLocalLogin = true; //TODO: We can get default from a setting?
if (context?.ClientId != null)
{
var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
if (client != null)
{
EnableLocalLogin = client.EnableLocalLogin;
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
{
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
}
}
}
ExternalProviders = providers.ToArray();
if (IsExternalLoginOnly)
{
//return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);
throw new NotImplementedException();
}
}
[UnitOfWork] //TODO: Will be removed when we implement action filter
public virtual async Task<IActionResult> OnPostAsync(string action)
{
if (action == "Cancel")
{
var context = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (context == null)
{
return Redirect("~/");
}
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
return Redirect(ReturnUrl);
}
ValidateModel();
var result = await _signInManager.PasswordSignInAsync(
LoginInput.UserNameOrEmailAddress,
LoginInput.Password,
LoginInput.RememberMe,
true
);
//TODO: Find a way of getting user's id from the logged in user and do not query it again like that!
var user = await _userManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ??
await _userManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
if (!result.Succeeded)
{
await _identityServerEvents.RaiseAsync(new UserLoginFailureEvent(LoginInput.UserNameOrEmailAddress, "Login failed: " + result.GetResultAsString()));
throw new UserFriendlyException("Login failed!"); //TODO: Handle other cases, do not throw exception
}
Debug.Assert(user != null, nameof(user) + " != null");
await _identityServerEvents.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName)); //TODO: Use user's name once implemented
if (!_interaction.IsValidReturnUrl(ReturnUrl))
{
return Redirect("~/");
}
return Redirect(ReturnUrl); //ReturnUrlHash?
}
[UnitOfWork]
public virtual async Task<IActionResult> OnPostExternalLogin(string provider)
{
if (_accountOptions.WindowsAuthenticationSchemeName == provider)
{
return await ProcessWindowsLoginAsync();
}
var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
properties.Items["scheme"] = provider;
return Challenge(properties, provider);
}
[UnitOfWork]
public virtual async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
{
//TODO: Did not implemented Identity Server 4 sample for this method (see ExternalLoginCallback in Quickstart of IDS4 sample)
/* Also did not implement these:
* - Logout(string logoutId)
*/
if (remoteError != null)
{
Logger.LogWarning($"External login callback error: {remoteError}");
return RedirectToPage("./Login");
}
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
Logger.LogWarning("External login info is not available");
return RedirectToPage("./Login");
}
var result = await _signInManager.ExternalLoginSignInAsync(
loginInfo.LoginProvider,
loginInfo.ProviderKey,
isPersistent: false,
bypassTwoFactor: true
);
if (result.IsLockedOut)
{
throw new UserFriendlyException("Cannot proceed because user is locked out!");
}
if (result.Succeeded)
{
return RedirectSafely(returnUrl, returnUrlHash);
}
//TODO: Handle other cases for result!
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
throw new ApplicationException("Error loading external login information during confirmation.");
}
var user = await CreateExternalUserAsync(info);
await _signInManager.SignInAsync(user, false);
return RedirectSafely(returnUrl, returnUrlHash);
}
private async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
var emailAddress = info.Principal.FindFirstValue(ClaimTypes.Email);
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await _userManager.CreateAsync(user));
CheckIdentityErrors(await _userManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await _userManager.AddLoginAsync(user, info));
return user;
}
private async Task<IActionResult> ProcessWindowsLoginAsync()
{
var result = await HttpContext.AuthenticateAsync(_accountOptions.WindowsAuthenticationSchemeName);
if (!(result?.Principal is WindowsPrincipal windowsPrincipal))
{
return Challenge(_accountOptions.WindowsAuthenticationSchemeName);
}
var props = new AuthenticationProperties
{
RedirectUri = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }),
Items =
{
{"scheme", _accountOptions.WindowsAuthenticationSchemeName},
}
};
var identity = new ClaimsIdentity(_accountOptions.WindowsAuthenticationSchemeName);
identity.AddClaim(new Claim(JwtClaimTypes.Subject, windowsPrincipal.Identity.Name));
identity.AddClaim(new Claim(JwtClaimTypes.Name, windowsPrincipal.Identity.Name));
//TODO: Consider to add Windows groups the the identity
//if (_accountOptions.IncludeWindowsGroups)
//{
// var windowsIdentity = windowsPrincipal.Identity as WindowsIdentity;
// if (windowsIdentity != null)
// {
// var groups = windowsIdentity.Groups?.Translate(typeof(NTAccount));
// var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
// identity.AddClaims(roles);
// }
//}
await HttpContext.SignInAsync(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(identity),
props
);
return RedirectSafely(props.RedirectUri);
}
public class LoginInputModel
{
[Required]
[StringLength(255)]
[Display(Name = "UserNameOrEmailAddress")]
public string UserNameOrEmailAddress { get; set; }
[Required]
[StringLength(32)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "RememberMe")]
public bool RememberMe { get; set; }
}
public class ExternalProviderModel
{
public string DisplayName { get; set; }
public string AuthenticationScheme { get; set; }
}
}
}

@ -0,0 +1,28 @@
@using Volo.Abp.Account.Web.Pages
@using Volo.Abp.Account.Web.Pages.Account
@model ConsentModel.ScopeViewModel
@* TODO: Should re-format this, just made copy/paste *@
<li class="list-group-item">
<div class="form-check">
<label asp-for="Checked" class="form-check-label">
<input asp-for="Checked" class="form-check-input" disabled="@Model.Required" />
@Html.DisplayNameFor(m => m.Checked)
<input asp-for="Name" type="hidden" />
<strong>@Model.DisplayName</strong>
@if (Model.Emphasize)
{
<span class="glyphicon glyphicon-exclamation-sign"></span>
}
</label>
</div>
@if (Model.Required)
{
<span><em>(required)</em></span>
}
@if (Model.Description != null)
{
<div class="consent-description">
<label for="scopes_@Model.Name">@Model.Description</label>
</div>
}
</li>

@ -0,0 +1,2 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap

@ -0,0 +1,113 @@
@page
@using Volo.Abp.Account.Web.Pages
@using Volo.Abp.Account.Web.Pages.Account
@model ConsentModel
<abp-card id="IdentityServerConsentWrapper">
<abp-card-header>
<div class="row">
<div class="col-md-12">
<h2>
@if (Model.ClientInfo.ClientLogoUrl != null)
{
<img src="@Model.ClientInfo.ClientLogoUrl">
}
@Model.ClientInfo.ClientName
<small>is requesting your permission</small>
</h2>
</div>
</div>
</abp-card-header>
<abp-card-body>
<form method="post" asp-page="/Consent">
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="ReturnUrlHash" />
<div>Uncheck the permissions you do not wish to grant.</div>
@if (Model.ConsentInput.IdentityScopes.Any())
{
<h3>Personal Information</h3>
<ul class="list-group">
@for (var i = 0; i < Model.ConsentInput.IdentityScopes.Count; i++)
{
<li class="list-group-item">
<div class="form-check">
<label asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-label">
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-input" />
@Model.ConsentInput.IdentityScopes[i].DisplayName
@if (Model.ConsentInput.IdentityScopes[i].Required)
{
<span><em>(required)</em></span>
}
</label>
</div>
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@
@if (Model.ConsentInput.IdentityScopes[i].Description != null)
{
<div class="consent-description">
@Model.ConsentInput.IdentityScopes[i].Description
</div>
}
</li>
}
</ul>
}
@if (Model.ConsentInput.ApiScopes.Any())
{
<h3>Application Access</h3>
<ul class="list-group">
@for (var i = 0; i < Model.ConsentInput.ApiScopes.Count; i++)
{
<li class="list-group-item">
<div class="form-check">
<label asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-label">
<input asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-input" disabled="@Model.ConsentInput.ApiScopes[i].Required" />
@Model.ConsentInput.ApiScopes[i].DisplayName
@if (Model.ConsentInput.ApiScopes[i].Required)
{
<span><em>(required)</em></span>
}
</label>
</div>
<input asp-for="@Model.ConsentInput.ApiScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@
@if (Model.ConsentInput.ApiScopes[i].Description != null)
{
<div class="consent-description">
@Model.ConsentInput.ApiScopes[i].Description
</div>
}
</li>
}
</ul>
}
@if (Model.ClientInfo.AllowRememberConsent)
{
<div class="form-check">
<label asp-for="@Model.ConsentInput.RememberConsent" class="form-check-label">
<input asp-for="@Model.ConsentInput.RememberConsent" class="form-check-input" />
<strong>Remember My Decision</strong>
</label>
</div>
}
<div>
<button name="UserDecision" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="UserDecision" value="no" class="btn">No, Do Not Allow</button>
@if (Model.ClientInfo.ClientUrl != null)
{
<a class="pull-right btn btn-secondary" target="_blank" href="@Model.ClientInfo.ClientUrl">
<strong>@Model.ClientInfo.ClientName</strong>
</a>
}
</div>
<div asp-validation-summary="All" class="text-danger"></div>
</form>
</abp-card-body>
</abp-card>

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.RazorPages;
using Volo.Abp.Ui;
namespace Volo.Abp.Account.Web.Pages
{
//TODO: Move this into the Account folder!!!
public class ConsentModel : AbpPageModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; }
[BindProperty]
public ConsentModel.ConsentInputModel ConsentInput { get; set; }
public ClientInfoModel ClientInfo { get; set; }
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IResourceStore _resourceStore;
public ConsentModel(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IResourceStore resourceStore)
{
_interaction = interaction;
_clientStore = clientStore;
_resourceStore = resourceStore;
}
public virtual async Task OnGet()
{
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (request == null)
{
throw new ApplicationException($"No consent request matching request: {ReturnUrl}");
}
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
if (client == null)
{
throw new ApplicationException($"Invalid client id: {request.ClientId}");
}
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
if (resources == null || (!resources.IdentityResources.Any() && !resources.ApiResources.Any()))
{
throw new ApplicationException($"No scopes matching: {request.ScopesRequested.Aggregate((x, y) => x + ", " + y)}");
}
ClientInfo = new ClientInfoModel(client);
ConsentInput = new ConsentInputModel
{
RememberConsent = true,
IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, true)).ToList(),
ApiScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, true)).ToList()
};
if (resources.OfflineAccess)
{
ConsentInput.ApiScopes.Add(GetOfflineAccessScope(true));
}
}
public virtual async Task<IActionResult> OnPost(string userDecision)
{
var result = await ProcessConsentAsync();
if (result.IsRedirect)
{
return Redirect(result.RedirectUri);
}
if (result.HasValidationError)
{
//ModelState.AddModelError("", result.ValidationError);
throw new ApplicationException("Error: " + result.ValidationError);
}
throw new ApplicationException("Unknown Error!");
}
protected virtual async Task<ConsentModel.ProcessConsentResult> ProcessConsentAsync()
{
var result = new ConsentModel.ProcessConsentResult();
ConsentResponse grantedConsent;
if (ConsentInput.UserDecision == "no")
{
grantedConsent = ConsentResponse.Denied;
}
else
{
if (ConsentInput.IdentityScopes.Any() || ConsentInput.ApiScopes.Any())
{
grantedConsent = new ConsentResponse
{
RememberConsent = ConsentInput.RememberConsent,
ScopesConsented = ConsentInput.GetAllowedScopeNames()
};
}
else
{
throw new UserFriendlyException("You must pick at least one permission"); //TODO: How to handle this
}
}
if (grantedConsent != null)
{
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (request == null)
{
return result;
}
await _interaction.GrantConsentAsync(request, grantedConsent);
result.RedirectUri = ReturnUrl; //TODO: ReturnUrlHash?
}
return result;
}
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{
return new ConsentModel.ScopeViewModel
{
Name = identity.Name,
DisplayName = identity.DisplayName,
Description = identity.Description,
Emphasize = identity.Emphasize,
Required = identity.Required,
Checked = check || identity.Required
};
}
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
{
return new ConsentModel.ScopeViewModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description,
Emphasize = scope.Emphasize,
Required = scope.Required,
Checked = check || scope.Required
};
}
protected virtual ConsentModel.ScopeViewModel GetOfflineAccessScope(bool check)
{
return new ConsentModel.ScopeViewModel
{
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = "Offline Access", //TODO: Localize
Description = "Access to your applications and resources, even when you are offline",
Emphasize = true,
Checked = check
};
}
public class ConsentInputModel
{
public List<ConsentModel.ScopeViewModel> IdentityScopes { get; set; }
public List<ConsentModel.ScopeViewModel> ApiScopes { get; set; }
[Required]
public string UserDecision { get; set; }
public bool RememberConsent { get; set; }
public List<string> GetAllowedScopeNames()
{
return IdentityScopes.Union(ApiScopes).Where(s => s.Checked).Select(s => s.Name).ToList();
}
}
public class ScopeViewModel
{
[Required]
[HiddenInput]
public string Name { get; set; }
public bool Checked { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public bool Emphasize { get; set; }
public bool Required { get; set; }
}
public class ProcessConsentResult
{
public bool IsRedirect => RedirectUri != null;
public string RedirectUri { get; set; }
public bool HasValidationError => ValidationError != null;
public string ValidationError { get; set; }
}
public class ClientInfoModel
{
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public ClientInfoModel(Client client)
{
//TODO: Automap
ClientName = client.ClientId;
ClientUrl = client.ClientUri;
ClientLogoUrl = client.LogoUri;
AllowRememberConsent = client.AllowRememberConsent;
}
}
}
}

@ -0,0 +1,3 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap

@ -18,14 +18,10 @@
<ItemGroup>
<EmbeddedResource Include="Pages\**\*.*" Exclude="*.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Account\Controllers\" />
<Folder Include="Pages\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Account.Web\Volo.Abp.Account.Web.csproj" />
<ProjectReference Include="..\Volo.Abp.IdentityServer.Domain\Volo.Abp.IdentityServer.Domain.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,16 @@
namespace Volo.Abp.Account.Web
{
public class AbpAccountOptions
{
/// <summary>
/// Default value: <see cref="Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme"/>.
/// </summary>
public string WindowsAuthenticationSchemeName { get; set; }
public AbpAccountOptions()
{
//TODO: This makes us depend on the Microsoft.AspNetCore.Server.IISIntegration package.
WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme;
}
}
}

@ -12,12 +12,12 @@ namespace Volo.Abp.Account.Web
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpAccountWebModule>();
services.Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpAccountWebModule>("Volo.Abp.Account.Web");
});
services.AddAssemblyOf<AbpAccountWebModule>();
}
}
}

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.Identity;
using Volo.Abp.Security.Claims;
using Volo.Abp.Ui;
using Volo.Abp.Uow;
@ -122,7 +123,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
private async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
var emailAddress = info.Principal.FindFirstValue(ClaimTypes.Email);
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, CurrentTenant.Id);

@ -0,0 +1,2 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap

@ -19,6 +19,14 @@
<EmbeddedResource Include="Pages\**\*.*" Exclude="*.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="Pages\Account\_ViewImports.cshtml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj" />
<ProjectReference Include="..\Volo.Abp.Identity.Domain\Volo.Abp.Identity.Domain.csproj" />

@ -0,0 +1,27 @@
using Volo.Abp.AspNetCore.Authentication.OAuth.Claims;
using Volo.Abp.Security.Claims;
namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
{
public static class AbpClaimActionCollectionExtensions
{
public static void MapAbpClaimTypes(this ClaimActionCollection claimActions)
{
claimActions.MapJsonKey(AbpClaimTypes.Email, "email");
claimActions.MapJsonKey(AbpClaimTypes.UserName, "name");
claimActions.MapJsonKey(AbpClaimTypes.EmailVerified, "email_verified");
claimActions.MapJsonKey(AbpClaimTypes.PhoneNumber, "phone_number");
claimActions.MapJsonKey(AbpClaimTypes.PhoneNumberVerified, "phone_number_verified");
claimActions.MapJsonKeyMultiple(AbpClaimTypes.Role, "role");
claimActions.DeleteClaim("name");
claimActions.DeleteClaim("email");
}
public static void MapJsonKeyMultiple(this ClaimActionCollection claimActions, string claimType, string jsonKey)
{
claimActions.Add(new MultipleClaimAction(claimType, jsonKey));
}
}
}

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.AspNetCore.Authentication.OAuth</AssemblyName>
<PackageId>Volo.Abp.AspNetCore.Authentication.OAuth</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" />
<ProjectReference Include="..\Volo.Abp.Security\Volo.Abp.Security.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OAuth" Version="2.0.1" />
</ItemGroup>
</Project>

@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.Security;
namespace Volo.Abp.AspNetCore.Authentication.OAuth
{
[DependsOn(typeof(AbpSecurityModule))]
public class AbpAspNetCoreAuthenticationOAuthModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpAspNetCoreAuthenticationOAuthModule>();
}
}
}

@ -0,0 +1,40 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using Newtonsoft.Json.Linq;
namespace Volo.Abp.AspNetCore.Authentication.OAuth.Claims
{
public class MultipleClaimAction : ClaimAction
{
public MultipleClaimAction(string claimType, string jsonKey)
: base(claimType, jsonKey)
{
}
public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
{
var prop = userData?.Property(ValueType);
if (prop == null)
{
return;
}
var propValue = prop.Value;
switch (propValue.Type)
{
case JTokenType.String:
identity.AddClaim(new Claim(ClaimType, propValue.Value<string>(), ValueType, issuer));
break;
case JTokenType.Array:
foreach (var innterValue in propValue.Values<string>())
{
identity.AddClaim(new Claim(ClaimType, innterValue, ValueType, issuer));
}
break;
default:
throw new AbpException("Unhandled JTokenType: " + propValue.Type);
}
}
}
}

@ -81,7 +81,8 @@ namespace Volo.Abp.AspNetCore.Mvc
var resourceType = assemblyResources.GetOrDefault(type.Assembly);
return factory.Create(resourceType ?? type);
};
});
})
.AddViewLocalization(); //TODO: How to configure from the application? Also, consider to move to a UI module since APIs does not care about it.
services.ExecutePreConfiguredActions(mvcBuilder);

@ -37,7 +37,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped<IdentityRoleStore>();
services.TryAddScoped(typeof(IRoleStore<IdentityRole>), provider => provider.GetService(typeof(IdentityRoleStore)));
return services.AddIdentity<IdentityUser, IdentityRole>(setupAction);
return services.AddIdentity<IdentityUser, IdentityRole>(setupAction)
.AddDefaultTokenProviders();
//return services.AddIdentityCore<IdentityUser>(setupAction);
}
}

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap;
using Volo.Abp.AutoMapper;
@ -28,8 +29,6 @@ namespace Volo.Abp.Identity.Web
public override void ConfigureServices(IServiceCollection services)
{
services.AddAssemblyOf<AbpIdentityWebModule>();
services.Configure<NavigationOptions>(options =>
{
options.MenuContributors.Add(new AbpIdentityWebMainMenuContributor());
@ -39,7 +38,7 @@ namespace Volo.Abp.Identity.Web
{
options.FileSets.AddEmbedded<AbpIdentityWebModule>("Volo.Abp.Identity.Web");
});
services.Configure<AbpLocalizationOptions>(options =>
{
options.Resources.AddVirtualJson<IdentityResource>("en", "/Localization/Resources/AbpIdentity");
@ -49,6 +48,18 @@ namespace Volo.Abp.Identity.Web
{
options.AddProfile<AbpIdentityWebAutoMapperProfile>(validate: true);
});
services.Configure<RazorPagesOptions>(options =>
{
options.Conventions.AuthorizePage("/Identity/Users/Index", IdentityPermissions.Users.Default);
options.Conventions.AuthorizePage("/Identity/Users/CreateModal", IdentityPermissions.Users.Create);
options.Conventions.AuthorizePage("/Identity/Users/EditModal", IdentityPermissions.Users.Update);
options.Conventions.AuthorizePage("/Identity/Roles/Index", IdentityPermissions.Roles.Default);
options.Conventions.AuthorizePage("/Identity/Roles/CreateModal", IdentityPermissions.Roles.Create);
options.Conventions.AuthorizePage("/Identity/Roles/EditModal", IdentityPermissions.Roles.Update);
});
services.AddAssemblyOf<AbpIdentityWebModule>();
}
}
}

@ -10,7 +10,7 @@ using Volo.Abp.Security.Claims;
namespace Volo.Abp.IdentityServer
{
public static class AbpZeroIdentityServerBuilderExtensions
public static class AbpIdentityServerBuilderExtensions
{
public static IIdentityServerBuilder AddAbpIdentityServer(
this IIdentityServerBuilder builder,
@ -31,6 +31,7 @@ namespace Volo.Abp.IdentityServer
AbpClaimTypes.UserId = JwtClaimTypes.Subject;
AbpClaimTypes.UserName = JwtClaimTypes.Name;
AbpClaimTypes.Role = JwtClaimTypes.Role;
AbpClaimTypes.Email = JwtClaimTypes.Email;
}
if (options.UpdateJwtSecurityTokenHandlerDefaultInboundClaimTypeMap)
@ -38,6 +39,7 @@ namespace Volo.Abp.IdentityServer
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.UserId] = AbpClaimTypes.UserId;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.UserName] = AbpClaimTypes.UserName;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.Role] = AbpClaimTypes.Role;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.Email] = AbpClaimTypes.Email;
}
return builder;

@ -2,7 +2,6 @@
using Volo.Abp.AutoMapper;
using Volo.Abp.Identity;
using Volo.Abp.IdentityServer.Clients;
using Volo.Abp.IdentityServer.Temp;
using Volo.Abp.Modularity;
using Volo.Abp.Security;
@ -22,19 +21,23 @@ namespace Volo.Abp.IdentityServer
options.AddProfile<ClientAutoMapperProfile>(validate: true);
});
services.AddAssemblyOf<AbpIdentityServerDomainModule>();
AddIdentityServer(services);
services.AddAssemblyOf<AbpIdentityServerDomainModule>();
}
private static void AddIdentityServer(IServiceCollection services)
{
var identityServerBuilder = services.AddIdentityServer();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
});
identityServerBuilder
.AddDeveloperSigningCredential()
//.AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
//.AddInMemoryClients(IdentityServerConfig.GetClients())
.AddDeveloperSigningCredential() //TODO: Should be able to change this!
.AddAbpIdentityServer();
services.ExecutePreConfiguredActions(identityServerBuilder);

@ -9,26 +9,37 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity
{
//TODO: Implement multi-tenancy as like in old ABP
public class AbpProfileService : ProfileService<IdentityUser>
public class AbpProfileService : ProfileService<IdentityUser>
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public AbpProfileService(
IdentityUserManager userManager,
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory
) : base(userManager, claimsFactory)
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory,
IUnitOfWorkManager unitOfWorkManager)
: base(userManager, claimsFactory)
{
_unitOfWorkManager = unitOfWorkManager;
}
[UnitOfWork]
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
await base.GetProfileDataAsync(context);
using (var uow = _unitOfWorkManager.Begin())
{
await base.GetProfileDataAsync(context);
await uow.CompleteAsync();
}
}
[UnitOfWork]
public override async Task IsActiveAsync(IsActiveContext context)
{
await base.IsActiveAsync(context);
using (var uow = _unitOfWorkManager.Begin())
{
await base.IsActiveAsync(context);
await uow.CompleteAsync();
}
}
}
}

@ -28,6 +28,11 @@ namespace Volo.Abp.IdentityServer.Clients
CreateMap<IdentityResource, IdentityServer4.Models.IdentityResource>();
CreateMap<UserClaim, string>()
.ConstructUsing(src => src.Type)
.ReverseMap()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src));
CreateMap<ApiSecret, IdentityServer4.Models.Secret>();
CreateMap<ApiScope, IdentityServer4.Models.Scope>();

@ -2,33 +2,45 @@
namespace Volo.Abp.Security.Claims
{
//TODO: Instead of directly using static properties, can we just create an AbpClaimOptions and pass these values as defaults?
/// <summary>
/// Used to get ABP-specific claim type names.
/// </summary>
public static class AbpClaimTypes
{
/// <summary>
/// UserId.
/// Default: <see cref="ClaimTypes.Name"/>
/// </summary>
public static string UserName { get; set; } = ClaimTypes.Name;
/// <summary>
/// UserId.
/// Default: <see cref="ClaimTypes.NameIdentifier"/>
/// </summary>
public static string UserId { get; set; } = ClaimTypes.NameIdentifier;
/// <summary>
/// UserId.
/// Default: <see cref="ClaimTypes.Role"/>
/// </summary>
public static string Role { get; set; } = ClaimTypes.Role;
/// <summary>
/// UserId.
/// Default: <see cref="ClaimTypes.Email"/>
/// </summary>
public static string Email { get; set; } = ClaimTypes.Email;
/// <summary>
/// Default: "email_verified".
/// </summary>
public static string EmailVerified { get; set; } = "email_verified";
/// <summary>
/// Default: "phone_number".
/// </summary>
public static string PhoneNumber { get; set; } = "phone_number";
/// <summary>
/// Default: "phone_number_verified".
/// </summary>
public static string PhoneNumberVerified { get; set; } = "phone_number_verified";
}
}

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<AssemblyName>Volo.Abp.AspNetCore.Authentication.OAuth.Tests</AssemblyName>
<PackageId>Volo.Abp.AspNetCore.Authentication.OAuth.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Authentication.OAuth\Volo.Abp.AspNetCore.Authentication.OAuth.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
</ItemGroup>
</Project>

@ -0,0 +1,46 @@
using System.Linq;
using System.Security.Claims;
using Newtonsoft.Json.Linq;
using Shouldly;
using Volo.Abp.Security.Claims;
using Xunit;
namespace Volo.Abp.AspNetCore.Authentication.OAuth.Claims
{
public class MultipleClaimAction_Tests
{
[Fact]
public void Should_Set_Single_Value()
{
var jObject = JObject.Parse(@"{
""sub"": ""71054539-0e48-af28-5e7a-39e4e42d8ea5"",
""role"": ""admin""
}");
var claimsIdentity = new ClaimsIdentity();
new MultipleClaimAction(AbpClaimTypes.Role, "role").Run(jObject, claimsIdentity, null);
var claims = claimsIdentity.FindAll(AbpClaimTypes.Role).ToList();
claims.Count.ShouldBe(1);
claims[0].Value.ShouldBe("admin");
}
[Fact]
public void Should_Set_Multiple_Values()
{
var jObject = JObject.Parse(@"{
""sub"": ""71054539-0e48-af28-5e7a-39e4e42d8ea5"",
""role"": [
""admin"",
""moderator""
]
}");
var claimsIdentity = new ClaimsIdentity();
new MultipleClaimAction(AbpClaimTypes.Role, "role").Run(jObject, claimsIdentity, null);
var claims = claimsIdentity.FindAll(AbpClaimTypes.Role).ToList();
claims.Count.ShouldBe(2);
claims[0].Value.ShouldBe("admin");
claims[1].Value.ShouldBe("moderator");
}
}
}

@ -20,14 +20,6 @@ namespace Volo.Abp.AspNetCore.Mvc
)]
public class AbpAspNetCoreMvcTestModule : AbpModule
{
public override void PreConfigureServices(IServiceCollection services)
{
services.PreConfigure<IMvcBuilder>(builder =>
{
builder.AddViewLocalization();
});
}
public override void ConfigureServices(IServiceCollection services)
{
services.AddLocalization(); //TODO: Move to the framework..?

Loading…
Cancel
Save