├── .gitignore ├── IdentityServer4SignalR.sln ├── README.md ├── docker-compose.production.yml ├── docker-compose.yml ├── global.json ├── runDocker.bat └── src ├── ChatAPI ├── Actors │ └── SignalREchoActor.cs ├── Auth │ ├── JWTInitialization.cs │ └── JWTSignalRExtensions.cs ├── ChatAPI.xproj ├── Dockerfile ├── Hub │ └── EchoHub.cs ├── KatanaIApplicationBuilderExtensions.cs ├── Messages │ └── EchoRequest.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SignalRContractResolver.cs ├── Startup.cs ├── appsettings.json ├── project.json ├── project.lock.json └── web.config ├── IdentityServer ├── Config.cs ├── Dockerfile ├── IdentityServer.xproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── ILocalUserService.cs │ ├── LocalProfileService.cs │ ├── LocalUser.cs │ └── TestUserService.cs ├── Startup.cs ├── Utils │ └── UrlUtils.cs ├── Views │ ├── Account │ │ ├── LoggedOut.cshtml │ │ ├── Login.cshtml │ │ └── Logout.cshtml │ ├── Consent │ │ ├── Index.cshtml │ │ └── _ScopeListItem.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ └── _ValidationSummary.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── Web │ ├── Account │ │ ├── AccountController.cs │ │ ├── AccountOptions.cs │ │ ├── AccountService.cs │ │ ├── ExternalProvider.cs │ │ ├── LoggedOutViewModel.cs │ │ ├── LoginInputModel.cs │ │ ├── LoginViewModel.cs │ │ ├── LogoutInputModel.cs │ │ └── LogoutViewModel.cs │ ├── Consent │ │ ├── ConsentController.cs │ │ ├── ConsentInputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── ConsentService.cs │ │ ├── ConsentViewModel.cs │ │ ├── ProcessConsentResult.cs │ │ └── ScopeViewModel.cs │ ├── Home │ │ ├── ErrorViewModel.cs │ │ └── HomeController.cs │ └── SecurityHeadersAttribute.cs ├── appsettings.json ├── project.json ├── project.lock.json ├── web.config └── wwwroot │ ├── css │ ├── site.css │ ├── site.less │ └── site.min.css │ ├── favicon.ico │ ├── icon.jpg │ ├── icon.png │ ├── js │ └── signout-redirect.js │ └── lib │ ├── bootstrap │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ └── jquery │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map └── Web ├── .dockerignore ├── .gitignore ├── .idea ├── Web.iml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml ├── typescript-compiler.xml └── vcs.xml ├── Dockerfile ├── Web.njsproj ├── azure └── web.config ├── package.json ├── public ├── favicon.ico ├── index.html └── pace │ ├── dataurl.css │ └── pace.min.js ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── Home.tsx ├── config │ ├── env.ts │ ├── oidcConfig.ts │ └── triggers.ts ├── echo │ ├── echo.tsx │ ├── echoConnector.tsx │ ├── echoConstants.ts │ ├── echoDialog.tsx │ ├── messageList.tsx │ ├── notAuthenticated.tsx │ └── notConnectedToSignalR.tsx ├── epics.ts ├── errors │ └── error404.tsx ├── flash │ ├── flash.tsx │ ├── flashActions.ts │ ├── flashConnector.tsx │ ├── flashConstants.ts │ ├── flashMessage.tsx │ ├── flashReducers.ts │ └── withFlash.tsx ├── index.css ├── index.tsx ├── layout │ └── mainLayout.tsx ├── login │ ├── authComponentWrappers.tsx │ ├── loginCallback.tsx │ ├── loginLink.tsx │ └── logoutLink.tsx ├── react-signalr │ └── signalrConnector.tsx ├── reducers.ts ├── redux-oidc │ ├── oidcActions.ts │ ├── oidcComponentWrapper.tsx │ ├── oidcConstants.ts │ ├── oidcMiddleware.ts │ ├── oidcReducers.ts │ └── withAuthToken.tsx └── store.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin/ 3 | obj/ 4 | min/ 5 | *.psess 6 | *.vspx 7 | *.TMP 8 | tmp/ 9 | .vs/ 10 | 11 | *.Resharper 12 | *.user 13 | *.suo 14 | *.tss 15 | *~ 16 | 17 | #**/wwwroot 18 | 19 | #!**/wwwroot/web.config 20 | **/node_modules 21 | npm-debug.log 22 | **/bower_components 23 | **/dist 24 | **/build 25 | **/typings 26 | 27 | # Visual Studio 28 | .vs/restore.dg 29 | 30 | NDependOut/ 31 | TestResults/ 32 | TestResult.xml 33 | *.VisualState.xml 34 | CodeCoverage/ 35 | *.orig 36 | *.msi 37 | *.exe 38 | !NuGet.exe 39 | !NuGet.targets 40 | *.vdproj 41 | *~HEAD 42 | out.txt 43 | ._* 44 | 45 | 46 | 47 | # IDEA 48 | **/.idea/workspace.xml 49 | **/.idea/tasks.xml 50 | **/.idea/dictionaries/ 51 | **/.idea/libraries/ 52 | 53 | .listen_test 54 | *codekit-config.json 55 | 56 | #nuget 57 | #.nuget/ 58 | #!NuGet.exe 59 | 60 | packages/ 61 | EF/ 62 | *.stackdump 63 | *.favdoc 64 | node_modules 65 | npm-debug.log 66 | 67 | # TEMP 68 | *.bak 69 | 70 | -------------------------------------------------------------------------------- /IdentityServer4SignalR.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "IdentityServer", "src\IdentityServer\IdentityServer.xproj", "{74894537-E514-4087-B6E1-B9D77A2ED34C}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D50701EB-5066-4C6B-B8D9-86AD49A4B29D}" 9 | ProjectSection(SolutionItems) = preProject 10 | .dockerignore = .dockerignore 11 | docker-compose.production.yml = docker-compose.production.yml 12 | docker-compose.yml = docker-compose.yml 13 | global.json = global.json 14 | README.md = README.md 15 | runDocker.bat = runDocker.bat 16 | EndProjectSection 17 | EndProject 18 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "Web", "src\Web\Web.njsproj", "{BD437718-9CB1-4820-A89D-79D58C5495C5}" 19 | EndProject 20 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ChatAPI", "src\ChatAPI\ChatAPI.xproj", "{69C5100A-A7F0-4597-8F1D-FFF0EAC7A74A}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {74894537-E514-4087-B6E1-B9D77A2ED34C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {74894537-E514-4087-B6E1-B9D77A2ED34C}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {74894537-E514-4087-B6E1-B9D77A2ED34C}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {74894537-E514-4087-B6E1-B9D77A2ED34C}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {BD437718-9CB1-4820-A89D-79D58C5495C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {BD437718-9CB1-4820-A89D-79D58C5495C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {BD437718-9CB1-4820-A89D-79D58C5495C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {BD437718-9CB1-4820-A89D-79D58C5495C5}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {69C5100A-A7F0-4597-8F1D-FFF0EAC7A74A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {69C5100A-A7F0-4597-8F1D-FFF0EAC7A74A}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {69C5100A-A7F0-4597-8F1D-FFF0EAC7A74A}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {69C5100A-A7F0-4597-8F1D-FFF0EAC7A74A}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## IdentityServer4 and SignalR 2 | 3 | This is an example of how to generate JWT tokens using 4 | IdentityServer4 and use them to authenticate users in SignalR via a React/TypeScript Single Page App. 5 | It will accompany [this blog post](https://mikebridge.github.io/articles/identityserver4-signalr/). 6 | 7 | ### Prerequisites: 8 | 9 | You can run this from the command line if you have Node and Dotnet Core installed, 10 | or you can use VS2015 or greater: 11 | 12 | - Install [Visual Studio 2015 Update 3](https://www.visualstudio.com/en-us/news/releasenotes/vs2015-update3-vs) 13 | and/or [DotNet Core 1.1](https://www.microsoft.com/net/download/core#/current). 14 | 15 | - Install [Node](https://nodejs.org/en/). I'm using 6.9.5. 16 | 17 | - Optionally install the [Node extension for Visual Studio](https://www.visualstudio.com/vs/node-js/) 18 | 19 | ## Create and Install Asymmetric Keys 20 | 21 | From the Developer Command Prompt: 22 | 23 | ``` 24 | > makecert -n "CN=ExampleTest" -a sha256 -sv ExampleTest.pvk -r ExampleTest.cer 25 | > pvk2pfx -pvk ExampleTest.pvk -spc ExampleTest.cer -pfx ExampleTest.pfx 26 | ``` 27 | 28 | The pvk2pfx command combines the pvk and cer files into a single pfx file containing both the public and private 29 | keys for the certificate. The IdentityServer4 app will use the private key from the pfx to sign tokens. 30 | The .cer file containing the public key can be shared with other services for the purpose of signature validation. 31 | 32 | To install asymmetric keys: 33 | 34 | 1. Go to `Manage Computer Certificates` in Windows 35 | 2. Under `Certificates - Local Computer => Personal => Certificates`, right click and select `All Tasks => Import...` 36 | 3. Select `ExampleTest.pfx` and import it (there's no password). You should see ExampleTest in the list. 37 | 4. Under `Certificates - Local Computer => Trusted People => Certificates`, right click and select `All Tasks => Import...` 38 | 5. Select `ExampleTest.cer` and import it (there's no password). 39 | 40 | If you want to verify generated JWT tokens yourself at [jwt.io](https://jwt.io/), you can translate the .pfx to 41 | a .pem file: 42 | 43 | ```bash 44 | openssl x509 -inform der -in ExampleTest.cer -pubkey -noout > ExampleTest_pub.pem 45 | ``` 46 | 47 | ### Launch from Command Line: 48 | 49 | From one console: 50 | 51 | ```cmd 52 | > cd src\ChatAPI 53 | > dotnet restore 54 | > dotnet run 55 | ``` 56 | 57 | And in another console: 58 | 59 | ```cmd 60 | > cd src\IdentityServer 61 | > dotnet restore 62 | > dotnet run 63 | ``` 64 | 65 | And in a third console: 66 | 67 | ```cmd 68 | > cd src\Web 69 | > npm install 70 | > npm run start 71 | ``` 72 | 73 | In a browser, navigate to [http://localhost:3000/](http://localhost:3000/). 74 | 75 | The two test users are "lou" and "bud" with the password "password". 76 | 77 | ## TODO 78 | 79 | I have not been able to get a net461 app working in docker using a nanoserver image. 80 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/docker-compose.production.yml -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | # identityserver: 5 | # build: ./src/IdentityServer 6 | # environment: 7 | # - ASPNETCORE_ENVIRONMENT=Development 8 | # ports: 9 | # - "5004:5004" 10 | 11 | 12 | chatapi: 13 | build: ./src/ChatAPI 14 | environment: 15 | - ASPNETCORE_ENVIRONMENT=Development 16 | ports: 17 | - "5000:5000" 18 | 19 | 20 | # web: 21 | # build: ./src/Web 22 | # environment: 23 | # - ASPNETCORE_ENVIRONMENT=Development 24 | # ports: 25 | # - "3000:3000" 26 | 27 | # networks: 28 | # - front 29 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src" ] 3 | } -------------------------------------------------------------------------------- /runDocker.bat: -------------------------------------------------------------------------------- 1 | dotnet restore 2 | 3 | dotnet build src\IdentityServer 4 | dotnet build src\ChatAPI 5 | 6 | dotnet publish src\IdentityServer 7 | dotnet publish src\ChatAPI 8 | 9 | docker-compose build 10 | 11 | -------------------------------------------------------------------------------- /src/ChatAPI/Actors/SignalREchoActor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Actor; 3 | using ChatAPI.Hub; 4 | using ChatAPI.Messages; 5 | using Microsoft.AspNet.SignalR; 6 | 7 | namespace ChatAPI.Actors 8 | { 9 | public class SignalREchoActor : TypedActor, 10 | IHandle 11 | { 12 | 13 | private IHubContext _context; 14 | 15 | protected override void PreStart() 16 | { 17 | _context = GlobalHost.ConnectionManager.GetHubContext(); 18 | } 19 | 20 | public void Handle(EchoRequest message) 21 | { 22 | Console.WriteLine($"handing message \"{message}\""); 23 | // send the message back to all clients. 24 | _context.Clients.All.echoMessage(message); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ChatAPI/Auth/JWTInitialization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography.X509Certificates; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace ChatAPI.Auth 9 | { 10 | public static class JWTInitialization 11 | { 12 | 13 | public static void Initialize(IApplicationBuilder app, IConfigurationRoot configuration) 14 | { 15 | ConfigureJwtAuth(app, configuration); 16 | } 17 | 18 | private static X509Certificate2 GetCertificateFromStore(string certName) 19 | { 20 | 21 | // Get the certificate store for the current user. 22 | //using (X509Store store = new X509Store(StoreName.TrustedPeople, StoreLocation.CurrentUser)) 23 | using (X509Store store = new X509Store(StoreName.TrustedPeople)) 24 | { 25 | store.Open(OpenFlags.ReadOnly); 26 | 27 | // Place all certificates in an X509Certificate2Collection object. 28 | X509Certificate2Collection certCollection = store.Certificates; 29 | 30 | // If using a certificate with a trusted root you do not need to FindByTimeValid, instead: 31 | // currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, certName, true); 32 | X509Certificate2Collection currentCerts = certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, 33 | false); 34 | X509Certificate2Collection signingCert = currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, 35 | certName, false); 36 | if (signingCert.Count == 0) 37 | return null; 38 | // Return the first certificate in the collection, has the right name and is current. 39 | return signingCert[0]; 40 | } 41 | } 42 | 43 | private static void ConfigureJwtAuth(IApplicationBuilder app, IConfigurationRoot configuration) 44 | { 45 | var jwtAppSettingOptions = configuration.GetSection("JwtIssuerOptions"); 46 | 47 | var selector = jwtAppSettingOptions["CertName"]; 48 | if (selector == null) 49 | { 50 | throw new Exception("The CertName is not configured in the appsettings"); 51 | } 52 | var cert = GetCertificateFromStore(selector); 53 | 54 | if (cert == null) 55 | { 56 | Console.Error.WriteLine("Unable to find cert for " + selector); 57 | throw new Exception("Unable to find cert for " + selector); 58 | } 59 | 60 | var tokenValidationParameters = new TokenValidationParameters 61 | { 62 | ValidateIssuer = true, 63 | ValidIssuer = jwtAppSettingOptions["Issuer"], 64 | ValidateAudience = true, 65 | ValidAudience = jwtAppSettingOptions["Audience"], 66 | ValidateIssuerSigningKey = true, 67 | IssuerSigningKeys = new List 68 | { 69 | new X509SecurityKey(cert) 70 | // If there are multiple valid keys, configure them here, 71 | // e.g. during a key migration. 72 | }, 73 | RequireExpirationTime = true, 74 | ValidateLifetime = true, 75 | ClockSkew = TimeSpan.Zero 76 | }; 77 | 78 | // add middleware to translate the query string token 79 | // passed by SignalR into an Authorization Bearer header 80 | app.UseJwtSignalRAuthentication(); 81 | 82 | app.UseJwtBearerAuthentication(new JwtBearerOptions 83 | { 84 | AutomaticAuthenticate = true, 85 | AutomaticChallenge = true, 86 | TokenValidationParameters = tokenValidationParameters 87 | }); 88 | 89 | 90 | } 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ChatAPI/Auth/JWTSignalRExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Akka.Util.Internal; 4 | using Microsoft.AspNetCore.Builder; 5 | 6 | namespace ChatAPI.Auth 7 | { 8 | 9 | /// 10 | /// Middleware to intercept a query string bearer token value (since SignalR isn't 11 | /// able to use a Header) into an auth header so that the Jwt header can handle it. 12 | /// 13 | public static class JwtSignalRExtensions 14 | { 15 | private static readonly String AUTH_QUERY_STRING_KEY = "authtoken"; 16 | 17 | public static void UseJwtSignalRAuthentication(this IApplicationBuilder app) 18 | { 19 | app.Use(async (context, next) => 20 | { 21 | if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"])) 22 | { 23 | try 24 | { 25 | if (context.Request.QueryString.HasValue) 26 | { 27 | 28 | var token = context.Request.QueryString.Value 29 | .Split('&') 30 | .SingleOrDefault(x => x.Contains(AUTH_QUERY_STRING_KEY))? 31 | .Split('=') 32 | .Drop(1) 33 | .First(); 34 | 35 | if (!string.IsNullOrWhiteSpace(token)) 36 | { 37 | context.Request.Headers.Add("Authorization", new[] {$"Bearer {token}"}); 38 | } 39 | 40 | } 41 | 42 | } 43 | catch 44 | { 45 | // if multiple headers it may throw an error. Ignore both. 46 | } 47 | } 48 | await next.Invoke(); 49 | }); 50 | 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ChatAPI/ChatAPI.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | 69c5100a-a7f0-4597-8f1d-fff0eac7a74a 11 | ChatAPI 12 | .\obj 13 | .\bin\ 14 | v4.6.1 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ChatAPI/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/nanoserver 2 | 3 | # This doesn't work---it seems to fail with no error message when executing 4 | # the .exe. 5 | 6 | SHELL ["powershell"] 7 | 8 | RUN [System.Environment]::OSVersion 9 | 10 | RUN Get-CimInstance win32_operatingsystem | Select-Object Version 11 | 12 | RUN new-item c:\ChatAPI -itemtype directory 13 | COPY .\\bin\\Debug\\net461\\win7-x64\\publish ChatAPI 14 | 15 | EXPOSE 5000 16 | 17 | # commented out for debugging 18 | #ENTRYPOINT ["C:\\ChatAPI", "ChatAPI.exe"] 19 | ENTRYPOINT ["powershell"] 20 | -------------------------------------------------------------------------------- /src/ChatAPI/Hub/EchoHub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using Akka.Actor; 5 | using ChatAPI.Messages; 6 | using Microsoft.AspNet.SignalR; 7 | using Microsoft.AspNet.SignalR.Owin; 8 | // ReSharper disable UnusedMember.Global 9 | // ReSharper disable ClassNeverInstantiated.Global 10 | 11 | namespace ChatAPI.Hub 12 | { 13 | [Authorize(Roles = "chatapi.user")] 14 | public class EchoHub : Microsoft.AspNet.SignalR.Hub 15 | { 16 | 17 | public void SendMessage(String message) 18 | { 19 | // uses 20 | var actorSystem = FindActorSystem(); 21 | var echoActorRef = actorSystem.ActorSelection("/user/echoActor"); 22 | echoActorRef.Tell(new EchoRequest(message, GetClaim("name"))); 23 | 24 | } 25 | 26 | private string GetClaim(string key) 27 | { 28 | var identity = Context.User?.Identity as ClaimsIdentity; 29 | var name = identity?.Claims 30 | .Where(claim => claim.Type == key) 31 | .Select(claim => claim.Value) 32 | .FirstOrDefault(); 33 | return name; 34 | } 35 | 36 | private ActorSystem FindActorSystem() 37 | { 38 | var ctx = Context.Request as ServerRequest; 39 | if (ctx == null) 40 | { 41 | throw new Exception("The context was not initialized"); 42 | } 43 | var actorSystem = ctx.Environment["akka.actorsystem"] as ActorSystem; 44 | if (actorSystem == null) 45 | { 46 | throw new Exception("The ActorSystem was not initialized"); 47 | } 48 | return actorSystem; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ChatAPI/KatanaIApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Owin.Builder; 6 | using Owin; 7 | 8 | namespace ChatAPI 9 | { 10 | using AppFunc = Func, Task>; 11 | 12 | // SEE: https://github.com/aspnet/Entropy/blob/dev/samples/Owin.IAppBuilderBridge/KAppBuilderExtensions.cs 13 | public static class KatanaIApplicationBuilderExtensions 14 | { 15 | 16 | public static IApplicationBuilder UseAppBuilder(this IApplicationBuilder app, Action configure) 17 | { 18 | app.UseOwin(addToPipeline => 19 | { 20 | addToPipeline(next => 21 | { 22 | var appBuilder = new AppBuilder(); 23 | appBuilder.Properties["builder.DefaultApp"] = next; 24 | 25 | configure(appBuilder); 26 | 27 | return appBuilder.Build(); 28 | }); 29 | }); 30 | return app; 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/ChatAPI/Messages/EchoRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ChatAPI.Messages 4 | { 5 | public class EchoRequest 6 | { 7 | public string Message { get; } 8 | 9 | public string FullName { get; } 10 | 11 | public EchoRequest(String message, String fullName) 12 | { 13 | Message = message; 14 | FullName = fullName; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ChatAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | namespace ChatAPI 6 | { 7 | // ReSharper disable once ClassNeverInstantiated.Global 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | var listen = "http://0.0.0.0:5000"; 13 | Console.WriteLine($"Starting on {listen}"); 14 | 15 | var host = new WebHostBuilder() 16 | .UseKestrel() 17 | .UseUrls(listen) // for docker to work, don't use localhost 18 | .UseContentRoot(Directory.GetCurrentDirectory()) 19 | .UseIISIntegration() 20 | .UseStartup() 21 | .Build(); 22 | 23 | Console.WriteLine("running..."); 24 | host.Run(); 25 | Console.WriteLine("started..."); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ChatAPI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | "ChatAPI": { 18 | "commandName": "Project", 19 | "launchUrl": "http://localhost:5000", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/ChatAPI/SignalRContractResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.AspNet.SignalR.Infrastructure; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace ChatAPI 7 | { 8 | 9 | /// 10 | /// Taken from 11 | /// http://stackoverflow.com/questions/30005575/signalr-use-camel-case#answer-30019100 12 | /// 13 | public class SignalRContractResolver : IContractResolver 14 | { 15 | 16 | private readonly Assembly _assembly; 17 | private readonly IContractResolver _camelCaseContractResolver; 18 | private readonly IContractResolver _defaultContractSerializer; 19 | 20 | public SignalRContractResolver() 21 | { 22 | _defaultContractSerializer = new DefaultContractResolver(); 23 | _camelCaseContractResolver = new CamelCasePropertyNamesContractResolver(); 24 | _assembly = typeof(Connection).Assembly; 25 | } 26 | 27 | public JsonContract ResolveContract(Type type) 28 | { 29 | return type.Assembly.Equals(_assembly) 30 | ? _defaultContractSerializer.ResolveContract(type) 31 | : _camelCaseContractResolver.ResolveContract(type); 32 | } 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/ChatAPI/Startup.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using ChatAPI.Actors; 3 | using ChatAPI.Auth; 4 | using Microsoft.AspNet.SignalR; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | using Owin; 12 | 13 | //using Akka; 14 | 15 | namespace ChatAPI 16 | { 17 | // ReSharper disable once ClassNeverInstantiated.Global 18 | public class Startup 19 | { 20 | readonly ILogger _logger; 21 | 22 | public IConfigurationRoot Configuration { get; } 23 | 24 | public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory) 25 | { 26 | _logger = loggerFactory.CreateLogger(); 27 | _logger.LogDebug("Startup"); 28 | 29 | var builder = new ConfigurationBuilder() 30 | .SetBasePath(env.ContentRootPath) 31 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 32 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); 33 | 34 | builder.AddEnvironmentVariables(); 35 | Configuration = builder.Build(); 36 | } 37 | 38 | 39 | public void ConfigureServices(IServiceCollection services) 40 | { 41 | 42 | services.AddCors(); 43 | 44 | // NOTE: unhandled Exception: System.InvalidOperationException: Unable to resolve service for type 45 | // 'System.Text.Encodings.Web.UrlEncoder' while attempting to activate 46 | // 'Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware' 47 | // is resolved by AddAuthentication (esp. if it's not brought in by MVC config) 48 | services.AddAuthentication(); 49 | 50 | // ensure that the PascalCased C# objects map to camelCased JSON objects 51 | SetCamelCaseSerialization(); 52 | } 53 | 54 | private static void SetCamelCaseSerialization() 55 | { 56 | var settings = new JsonSerializerSettings 57 | { 58 | ContractResolver = new SignalRContractResolver() 59 | }; 60 | var serializer = JsonSerializer.Create(settings); 61 | GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => serializer); 62 | } 63 | 64 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 65 | { 66 | loggerFactory.AddConsole(); 67 | 68 | if (env.IsDevelopment()) 69 | { 70 | app.UseDeveloperExceptionPage(); 71 | } 72 | 73 | JWTInitialization.Initialize(app, Configuration); 74 | 75 | app.UseCors(policy => 76 | { 77 | //policy.WithOrigins("*"); 78 | policy.WithOrigins("http://localhost:3000"); 79 | policy.AllowAnyHeader(); 80 | policy.AllowAnyMethod(); 81 | policy.AllowCredentials(); 82 | }); 83 | 84 | ConfigureAkka(app); 85 | 86 | } 87 | 88 | private void ConfigureAkka(IApplicationBuilder app) 89 | { 90 | _logger.LogInformation("Initializing Akka ActorSystem"); 91 | var actorSystem = ActorSystem.Create("SignalRChatAPI"); 92 | 93 | var echoActor = actorSystem.ActorOf(Props.Create(() => new SignalREchoActor()), "echoActor"); 94 | 95 | app.UseAppBuilder(appBuilder => appBuilder.Use((ctx, next1) => 96 | { 97 | // make the actor system available via the owin environment 98 | ctx.Environment["akka.actorsystem"] = actorSystem; 99 | return next1(); 100 | }).MapSignalR()); 101 | 102 | } 103 | 104 | 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ChatAPI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "JwtIssuerOptions": { 3 | "Issuer": "http://localhost:5004", 4 | "Audience": "chatapi", 5 | "CertName": "CN=ExampleTest" 6 | } 7 | } -------------------------------------------------------------------------------- /src/ChatAPI/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.AspNetCore.Diagnostics": "1.0.0", 4 | "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", 5 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", 6 | "Microsoft.Extensions.Logging.Console": "1.0.0", 7 | "Microsoft.Extensions.Configuration.Json": "1.1.0", 8 | "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", 9 | 10 | "Microsoft.AspNetCore.Cors": "1.1.0", 11 | "Microsoft.AspNetCore.Mvc": "1.1.1", 12 | "Microsoft.AspNetCore.Mvc.Core": "1.1.1", 13 | "Microsoft.AspNetCore.Owin": "1.1.0", 14 | "Microsoft.IdentityModel.Tokens": "5.1.2", 15 | "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", 16 | "Microsoft.AspNet.SignalR": "2.2.1", 17 | "Akka": "1.1.3", 18 | "Akka.Remote": "1.1.3" 19 | 20 | }, 21 | 22 | "tools": { 23 | "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" 24 | }, 25 | 26 | "frameworks": { 27 | "net461": { 28 | } 29 | }, 30 | 31 | "buildOptions": { 32 | "emitEntryPoint": true, 33 | "preserveCompilationContext": true 34 | }, 35 | 36 | "runtimeOptions": { 37 | "configProperties": { 38 | "System.GC.Server": true 39 | } 40 | }, 41 | 42 | "publishOptions": { 43 | "include": [ 44 | "wwwroot", 45 | "web.config", 46 | "appsettings.json" 47 | ] 48 | }, 49 | 50 | "scripts": { 51 | "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ChatAPI/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/IdentityServer/Config.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Security.Claims; 6 | using IdentityServer.Utils; 7 | using IdentityServer4; 8 | using IdentityServer4.Models; 9 | using IdentityServer4.Test; 10 | 11 | namespace IdentityServer 12 | { 13 | public class Config 14 | { 15 | // TODO: Note that identity resources and access token resources 16 | // are configured separately. 17 | 18 | 19 | // scopes define the resources in your system 20 | public static IEnumerable GetIdentityResources() 21 | { 22 | var userProfile = new IdentityResource 23 | { 24 | Name = "profile.user", 25 | DisplayName = "User profile", 26 | UserClaims = new[] { "name" } 27 | }; 28 | return new List 29 | { 30 | new IdentityResources.OpenId(), 31 | new IdentityResources.Email(), 32 | new IdentityResources.Profile(), 33 | userProfile 34 | }; 35 | } 36 | 37 | public static IEnumerable GetApiResources() 38 | { 39 | return new List 40 | { 41 | new ApiResource("chatapi", "Chat API"), 42 | }; 43 | } 44 | 45 | // clients want to access resources (aka scopes) 46 | public static IEnumerable GetClients() 47 | { 48 | const string baseUrl = "http://localhost:3000"; 49 | 50 | return new List 51 | { 52 | 53 | new Client 54 | { 55 | ClientId = "js.implicit", 56 | ClientName = "JavaScript Client", 57 | AllowedGrantTypes = GrantTypes.Implicit, 58 | AllowAccessTokensViaBrowser = true, 59 | 60 | RedirectUris = 61 | { 62 | // this where the word "implicit" in "implicit flow" 63 | // comes from---we are implicitly telling the 64 | // server that we own the domain that hosts 65 | // the callback page. The server knows the domain is valid 66 | // because the domain is whitelisted in the Client configuration. 67 | UrlUtils.UrlCombine(baseUrl, "/callback") 68 | }, 69 | PostLogoutRedirectUris = 70 | { 71 | baseUrl 72 | }, 73 | AllowedCorsOrigins = 74 | { 75 | baseUrl 76 | }, 77 | 78 | AllowedScopes = 79 | { 80 | IdentityServerConstants.StandardScopes.OpenId, 81 | IdentityServerConstants.StandardScopes.Profile, 82 | IdentityServerConstants.StandardScopes.Email, 83 | "chatapi" 84 | }, 85 | RequireConsent = false, // we don't want the "Consent" Screen 86 | AllowRememberConsent = false, 87 | AlwaysSendClientClaims = true, 88 | AlwaysIncludeUserClaimsInIdToken = true, 89 | AccessTokenType = AccessTokenType.Jwt, 90 | // UpdateAccessTokenClaimsOnRefresh = 91 | } 92 | 93 | }; 94 | } 95 | 96 | public static List GetUsers() 97 | { 98 | return new List 99 | { 100 | new TestUser 101 | { 102 | SubjectId = "1", 103 | Username = "alice", 104 | Password = "password", 105 | 106 | Claims = new List 107 | { 108 | new Claim("name", "Alice"), 109 | new Claim("website", "https://alice.com") 110 | } 111 | }, 112 | new TestUser 113 | { 114 | SubjectId = "2", 115 | Username = "bob", 116 | Password = "password", 117 | 118 | Claims = new List 119 | { 120 | new Claim("name", "Bob"), 121 | new Claim("website", "https://bob.com") 122 | } 123 | } 124 | }; 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/IdentityServer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:1.1.0 2 | #RUN apt-get update 3 | 4 | ENTRYPOINT ["dotnet", "IdentityServer.dll"] 5 | ARG source=./bin/Debug/netcoreapp1.1/publish 6 | WORKDIR /app 7 | EXPOSE 5000 8 | COPY $source . 9 | 10 | -------------------------------------------------------------------------------- /src/IdentityServer/IdentityServer.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 74894537-e514-4087-b6e1-b9d77a2ed34c 10 | IdentityServer 11 | .\obj 12 | .\bin\ 13 | v4.5.2 14 | 15 | 16 | 2.0 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/IdentityServer/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace IdentityServer 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | Console.Title = "IdentityServer"; 15 | 16 | var host = new WebHostBuilder() 17 | .UseKestrel() 18 | .UseUrls("http://0.0.0.0:5004") // for docker to work, don't use localhost 19 | .UseContentRoot(Directory.GetCurrentDirectory()) 20 | .UseIISIntegration() 21 | .UseStartup() 22 | .Build(); 23 | 24 | host.Run(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/IdentityServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5004/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | "IdentityServer": { 18 | "commandName": "Project", 19 | "launchUrl": "http://localhost:5000", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/IdentityServer/Services/ILocalUserService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace IdentityServer.Services 7 | { 8 | public interface ILocalUserService 9 | { 10 | Task FindAsync(Guid id); 11 | 12 | Task FindByLoginCrentialsAsync(string userName, string password); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/IdentityServer/Services/LocalProfileService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using IdentityModel; 7 | using IdentityServer4.Extensions; 8 | using IdentityServer4.Models; 9 | using IdentityServer4.Services; 10 | 11 | namespace IdentityServer.Services 12 | { 13 | public class LocalProfileService : IProfileService 14 | { 15 | private readonly ILocalUserService _userService; 16 | 17 | public LocalProfileService(ILocalUserService userService) 18 | { 19 | _userService = userService; 20 | } 21 | 22 | /// 23 | /// Side Effect: this mutates context.IssuedClaims with the user's identity properties if the 24 | /// user is found. Otherwise do nothing. 25 | /// 26 | /// 27 | /// 28 | public async Task GetProfileDataAsync(ProfileDataRequestContext context) 29 | { 30 | /* 31 | * see: https://damienbod.com/2016/11/18/extending-identity-in-identityserver4-to-manage-users-in-asp-net-core/ 32 | * The Identity properties need to be added to the claims so that the client SPA or whatever client it 33 | * is can use the properties. In IdentityServer4, the IProfileService interface is used for 34 | * this. Each custom ApplicationUser property is added as claims as required. 35 | */ 36 | 37 | 38 | var identityidString = context.Subject.GetSubjectId(); 39 | Guid identityid; 40 | bool success = Guid.TryParse(identityidString, out identityid); 41 | if (!success) 42 | { 43 | return; 44 | } 45 | 46 | var user = await _userService.FindAsync(identityid); 47 | 48 | if (user == null) 49 | { 50 | return; 51 | } 52 | var claims = new List 53 | { 54 | new Claim(JwtClaimTypes.FamilyName, user.FamilyName, ClaimValueTypes.String), 55 | new Claim(JwtClaimTypes.GivenName, user.GivenName, ClaimValueTypes.String), 56 | new Claim(JwtClaimTypes.Name, user.FullName, ClaimValueTypes.String), 57 | new Claim(JwtClaimTypes.Id, user.Id.ToString(), ClaimValueTypes.String), 58 | new Claim(JwtClaimTypes.PreferredUserName, user.UserName, ClaimValueTypes.String) 59 | }; 60 | // hard-code access to "chatapi.user"----normally this would come from a 61 | // database! 62 | claims.Add(new Claim(JwtClaimTypes.Role, "chatapi.user")); 63 | claims.Add(new Claim(JwtClaimTypes.Scope, "chatapi")); 64 | 65 | context.IssuedClaims = claims; 66 | 67 | } 68 | 69 | public async Task IsActiveAsync(IsActiveContext context) 70 | { 71 | // TODO: Make this check the active status of the user 72 | var sub = await Task.FromResult(context.Subject.GetSubjectId()); 73 | //var user = await _userManager.FindByIdAsync(sub); 74 | //context.IsActive = user != null; 75 | 76 | context.IsActive = true; 77 | 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/IdentityServer/Services/LocalUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace IdentityServer.Services 7 | { 8 | public class LocalUser 9 | { 10 | public Guid Id { get; set; } 11 | 12 | public String UserName { get; set; } 13 | 14 | public String GivenName { get; set; } 15 | 16 | public String FamilyName { get; set; } 17 | 18 | public String FullName => $"{GivenName ?? ""} {FamilyName ?? ""}".Trim(); 19 | 20 | public String EmailAddress { get; set; } 21 | 22 | // obviously in a production system we wouldn't store this in plain text! 23 | public String Password { get; set; } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/IdentityServer/Services/TestUserService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using IdentityServer4.Test; 6 | 7 | namespace IdentityServer.Services 8 | { 9 | public class TestUserService : ILocalUserService 10 | { 11 | private readonly IDictionary _users = new Dictionary(); 12 | 13 | public TestUserService() 14 | { 15 | InitTestData(); 16 | } 17 | 18 | public Task FindAsync(Guid id) 19 | { 20 | return Task.FromResult(_users.ContainsKey(id) ? _users[id] : null); 21 | } 22 | 23 | 24 | private void InitTestData() 25 | { 26 | var id1 = Guid.NewGuid(); 27 | _users.Add(id1, new LocalUser 28 | { 29 | EmailAddress = "lou@example.org", 30 | FamilyName = "Costello", 31 | GivenName = "Lou", 32 | UserName = "lou", 33 | Id= id1, 34 | Password = "password" 35 | }); 36 | var id2 = Guid.NewGuid(); 37 | _users.Add(id2, new LocalUser 38 | { 39 | EmailAddress = "bud@example.org", 40 | FamilyName = "Abbott", 41 | GivenName = "Bud", 42 | UserName = "bud", 43 | Id = id2, 44 | Password = "password" 45 | }); 46 | 47 | } 48 | 49 | public Task FindByLoginCrentialsAsync(string userName, string password) 50 | { 51 | return Task.FromResult(_users.Values.FirstOrDefault(user => user.UserName == userName && user.Password == password)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/IdentityServer/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | using System.Security.Cryptography.X509Certificates; 5 | using IdentityServer.Services; 6 | using IdentityServer4; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | // ReSharper disable ClassNeverInstantiated.Global 12 | // ReSharper disable UnusedMember.Global 13 | 14 | namespace IdentityServer 15 | { 16 | public class Startup 17 | { 18 | 19 | 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddMvc(); 23 | 24 | // configure identity server with in-memory stores, keys, clients and scopes 25 | services.AddIdentityServer() 26 | .AddSigningCredential("CN=ExampleTest") 27 | .AddInMemoryIdentityResources(Config.GetIdentityResources()) 28 | .AddInMemoryApiResources(Config.GetApiResources()) 29 | .AddInMemoryClients(Config.GetClients()) 30 | .AddProfileService(); 31 | 32 | // register our own testuser store 33 | services.AddSingleton(); 34 | } 35 | 36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 37 | { 38 | loggerFactory.AddConsole(LogLevel.Debug); 39 | app.UseDeveloperExceptionPage(); 40 | 41 | app.UseIdentityServer(); 42 | 43 | app.UseCookieAuthentication(new CookieAuthenticationOptions 44 | { 45 | AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme, 46 | AutomaticAuthenticate = false, 47 | AutomaticChallenge = false 48 | }); 49 | 50 | // this is the default example's Google integration 51 | // app.UseGoogleAuthentication(new GoogleOptions 52 | // { 53 | // AuthenticationScheme = "Google", 54 | // DisplayName = "Google", 55 | // SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme, 56 | // 57 | // ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com", 58 | // ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo" 59 | // }); 60 | 61 | app.UseStaticFiles(); 62 | app.UseMvcWithDefaultRoute(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/IdentityServer/Utils/UrlUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace IdentityServer.Utils 7 | { 8 | public class UrlUtils 9 | { 10 | public static string UrlCombine(string url1, string url2) 11 | { 12 | if (url1.Length == 0) { return url2; } 13 | 14 | if (url2.Length == 0) { return url1; } 15 | 16 | return string.Format("{0}/{1}", url1.TrimEnd('/', '\\'), url2.TrimStart('/', '\\')); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/LoggedOut.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.Web.Account 2 | @model IdentityServer.Web.Account.LoggedOutViewModel 3 | 4 | @{ 5 | // set this so the layout rendering sees an anonymous user 6 | ViewData["signed-out"] = true; 7 | } 8 | 9 | 28 | 29 | @section scripts 30 | { 31 | @if (Model.AutomaticRedirectAfterSignOut) 32 | { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.Web.Account 2 | @model IdentityServer.Web.Account.LoginViewModel 3 | 4 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.Web.Account 2 | @model IdentityServer.Web.Account.LogoutViewModel 3 | 4 |
5 | 8 | 9 |
10 |
11 |

Would you like to logout of IdentityServer?

12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /src/IdentityServer/Views/Consent/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.Web.Consent 2 | @model IdentityServer.Web.Consent.ConsentViewModel 3 | 4 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Consent/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.Web.Consent 2 | @model IdentityServer.Web.Consent.ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | (required) 28 | } 29 | @if (Model.Description != null) 30 | { 31 | 34 | } 35 |
  • -------------------------------------------------------------------------------- /src/IdentityServer/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | 
    2 | 11 | 12 |
    13 |
    14 |

    15 | IdentityServer publishes a 16 | discovery document 17 | where you can find metadata and links to all the endpoints, key material, etc. 18 |

    19 |
    20 |
    21 |
    22 |
    23 |

    24 | Here are links to the 25 | source code repository, 26 | and ready to use samples. 27 |

    28 |
    29 |
    30 |
    31 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.Web.Home 2 | @model IdentityServer.Web.Home.ErrorViewModel 3 | 4 | @{ 5 | var error = Model?.Error?.Error; 6 | var request_id = Model?.Error?.RequestId; 7 | } 8 | 9 |
    10 | 13 | 14 |
    15 |
    16 |
    17 | Sorry, there was an error 18 | 19 | @if (error != null) 20 | { 21 | 22 | 23 | : @error 24 | 25 | 26 | } 27 |
    28 | 29 | @if (request_id != null) 30 | { 31 |
    Request Id: @request_id
    32 | } 33 |
    34 |
    35 |
    36 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Extensions 2 | @{ 3 | string name = null; 4 | if (!true.Equals(ViewData["signed-out"])) 5 | { 6 | var user = await Context.GetIdentityServerUserAsync(); 7 | name = user?.FindFirst("name")?.Value; 8 | } 9 | } 10 | 11 | 12 | 13 | 14 | 15 | 16 | IdentityServer4 17 | 18 | 19 | 20 | 21 | 22 | 23 | 53 | 54 |
    55 | @RenderBody() 56 |
    57 | 58 | 59 | 60 | @RenderSection("scripts", required: false) 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /src/IdentityServer/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 2 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/AccountController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Security.Claims; 8 | using System.Security.Principal; 9 | using System.Threading.Tasks; 10 | using IdentityServer.Services; 11 | using IdentityServer4; 12 | using IdentityServer4.Services; 13 | using IdentityServer4.Stores; 14 | using Microsoft.AspNetCore.Http; 15 | using Microsoft.AspNetCore.Http.Authentication; 16 | using Microsoft.AspNetCore.Mvc; 17 | 18 | namespace IdentityServer.Web.Account 19 | { 20 | /// 21 | /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. 22 | /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! 23 | /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval 24 | /// 25 | [SecurityHeaders] 26 | public class AccountController : Controller 27 | { 28 | private readonly IIdentityServerInteractionService _interaction; 29 | private readonly ILocalUserService _userService; 30 | private readonly AccountService _account; 31 | 32 | public AccountController( 33 | IIdentityServerInteractionService interaction, 34 | IClientStore clientStore, 35 | IHttpContextAccessor httpContextAccessor, 36 | ILocalUserService userService) 37 | { 38 | _interaction = interaction; 39 | _userService = userService; 40 | _account = new AccountService(interaction, httpContextAccessor, clientStore); 41 | } 42 | 43 | /// 44 | /// Show login page 45 | /// 46 | [HttpGet] 47 | public async Task Login(string returnUrl) 48 | { 49 | var vm = await _account.BuildLoginViewModelAsync(returnUrl); 50 | 51 | // if (vm.IsExternalLoginOnly) 52 | // { 53 | // // only one option for logging in 54 | // return await ExternalLogin(vm.ExternalProviders.First().AuthenticationScheme, returnUrl); 55 | // } 56 | 57 | return View(vm); 58 | } 59 | 60 | /// 61 | /// Handle postback from username/password login 62 | /// 63 | [HttpPost] 64 | [ValidateAntiForgeryToken] 65 | public async Task Login(LoginInputModel model) 66 | { 67 | if (ModelState.IsValid) 68 | { 69 | // find the user from our local user store 70 | var localUser = await _userService.FindByLoginCrentialsAsync(model.Username, model.Password); 71 | if (localUser != null) 72 | { 73 | AuthenticationProperties props = null; 74 | // only set explicit expiration here if persistent. 75 | // otherwise we reply upon expiration configured in cookie middleware. 76 | if (AccountOptions.AllowRememberLogin && model.RememberLogin) 77 | { 78 | props = new AuthenticationProperties 79 | { 80 | IsPersistent = true, 81 | ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) 82 | }; 83 | }; 84 | 85 | // issue authentication cookie with subject ID and username 86 | // var user = _users.FindByUsername(model.Username); 87 | // // TODO: Move this to a LocalPasswordValidator 88 | // var optionalClaims = new List 89 | // { 90 | // new Claim(ClaimTypes.GivenName, "Foo"), 91 | // new Claim(ClaimTypes.Surname, "Bar"), 92 | // new Claim(ClaimTypes.Name, "Foo Bar") 93 | // //new Claim(ClaimTypes.StateOrProvince, identity.State) 94 | // //new Claim(ClaimTypes.Country, "ca"), 95 | // }; 96 | // var grantValidationResult = new GrantValidationResult( 97 | // 98 | // subject: "AAA", 99 | // authenticationMethod: "custom", 100 | // claims: optionalClaims); 101 | 102 | 103 | // END 104 | 105 | 106 | // TODO: localUser.fullname? Or Username? 107 | //await HttpContext.Authentication.SignInAsync(localUser.Id.ToString(), localUser.FullName, props, optionalClaims.ToArray()); 108 | await HttpContext.Authentication.SignInAsync(localUser.Id.ToString(), localUser.FullName, props); 109 | 110 | // make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint 111 | if (_interaction.IsValidReturnUrl(model.ReturnUrl)) 112 | { 113 | return Redirect(model.ReturnUrl); 114 | } 115 | 116 | return Redirect("~/"); 117 | } 118 | 119 | ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); 120 | } 121 | 122 | // something went wrong, show form with error 123 | var vm = await _account.BuildLoginViewModelAsync(model); 124 | return View(vm); 125 | } 126 | 127 | /// 128 | /// Show logout page 129 | /// 130 | [HttpGet] 131 | public async Task Logout(string logoutId) 132 | { 133 | var vm = await _account.BuildLogoutViewModelAsync(logoutId); 134 | 135 | if (vm.ShowLogoutPrompt == false) 136 | { 137 | // no need to show prompt 138 | return await Logout(vm); 139 | } 140 | 141 | return View(vm); 142 | } 143 | 144 | /// 145 | /// Handle logout page postback 146 | /// 147 | [HttpPost] 148 | [ValidateAntiForgeryToken] 149 | public async Task Logout(LogoutInputModel model) 150 | { 151 | var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId); 152 | if (vm.TriggerExternalSignout) 153 | { 154 | string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); 155 | try 156 | { 157 | // hack: try/catch to handle social providers that throw 158 | await HttpContext.Authentication.SignOutAsync(vm.ExternalAuthenticationScheme, 159 | new AuthenticationProperties { RedirectUri = url }); 160 | } 161 | catch(NotSupportedException) // this is for the external providers that don't have signout 162 | { 163 | } 164 | catch(InvalidOperationException) // this is for Windows/Negotiate 165 | { 166 | } 167 | } 168 | 169 | // delete local authentication cookie 170 | await HttpContext.Authentication.SignOutAsync(); 171 | 172 | return View("LoggedOut", vm); 173 | } 174 | 175 | /// 176 | /// initiate roundtrip to external authentication provider 177 | /// 178 | // [HttpGet] 179 | // public async Task ExternalLogin(string provider, string returnUrl) 180 | // { 181 | // returnUrl = Url.Action("ExternalLoginCallback", new { returnUrl = returnUrl }); 182 | // 183 | // // windows authentication is modeled as external in the asp.net core authentication manager, so we need special handling 184 | // if (AccountOptions.WindowsAuthenticationSchemes.Contains(provider)) 185 | // { 186 | // // but they don't support the redirect uri, so this URL is re-triggered when we call challenge 187 | // if (HttpContext.User is WindowsPrincipal) 188 | // { 189 | // var props = new AuthenticationProperties(); 190 | // props.Items.Add("scheme", HttpContext.User.Identity.AuthenticationType); 191 | // 192 | // var id = new ClaimsIdentity(provider); 193 | // id.AddClaim(new Claim(ClaimTypes.NameIdentifier, HttpContext.User.Identity.Name)); 194 | // id.AddClaim(new Claim(ClaimTypes.Name, HttpContext.User.Identity.Name)); 195 | // 196 | // await HttpContext.Authentication.SignInAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme, new ClaimsPrincipal(id), props); 197 | // return Redirect(returnUrl); 198 | // } 199 | // else 200 | // { 201 | // // this triggers all of the windows auth schemes we're supporting so the browser can use what it supports 202 | // return new ChallengeResult(AccountOptions.WindowsAuthenticationSchemes); 203 | // } 204 | // } 205 | // else 206 | // { 207 | // // start challenge and roundtrip the return URL 208 | // var props = new AuthenticationProperties 209 | // { 210 | // RedirectUri = returnUrl, 211 | // Items = { { "scheme", provider } } 212 | // }; 213 | // return new ChallengeResult(provider, props); 214 | // } 215 | // } 216 | 217 | /// 218 | /// Post processing of external authentication 219 | /// 220 | // [HttpGet] 221 | // public async Task ExternalLoginCallback(string returnUrl) 222 | // { 223 | // // read external identity from the temporary cookie 224 | // var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); 225 | // var tempUser = info?.Principal; 226 | // if (tempUser == null) 227 | // { 228 | // throw new Exception("External authentication error"); 229 | // } 230 | // 231 | // // retrieve claims of the external user 232 | // var claims = tempUser.Claims.ToList(); 233 | // 234 | // // try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier 235 | // // depending on the external provider, some other claim type might be used 236 | // var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject); 237 | // if (userIdClaim == null) 238 | // { 239 | // userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier); 240 | // } 241 | // if (userIdClaim == null) 242 | // { 243 | // throw new Exception("Unknown userid"); 244 | // } 245 | // 246 | // // remove the user id claim from the claims collection and move to the userId property 247 | // // also set the name of the external authentication provider 248 | // claims.Remove(userIdClaim); 249 | // var provider = info.Properties.Items["scheme"]; 250 | // var userId = userIdClaim.Value; 251 | // 252 | // // check if the external user is already provisioned 253 | // var user = _users.FindByExternalProvider(provider, userId); 254 | // if (user == null) 255 | // { 256 | // // this sample simply auto-provisions new external user 257 | // // another common approach is to start a registrations workflow first 258 | // user = _users.AutoProvisionUser(provider, userId, claims); 259 | // } 260 | // 261 | // var additionalClaims = new List(); 262 | // 263 | // // if the external system sent a session id claim, copy it over 264 | // var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); 265 | // if (sid != null) 266 | // { 267 | // additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); 268 | // } 269 | // 270 | // // if the external provider issued an id_token, we'll keep it for signout 271 | // AuthenticationProperties props = null; 272 | // var id_token = info.Properties.GetTokenValue("id_token"); 273 | // if (id_token != null) 274 | // { 275 | // props = new AuthenticationProperties(); 276 | // props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); 277 | // } 278 | // 279 | // // issue authentication cookie for user 280 | // await HttpContext.Authentication.SignInAsync(user.SubjectId, user.Username, provider, props, additionalClaims.ToArray()); 281 | // 282 | // // delete temporary cookie used during external authentication 283 | // await HttpContext.Authentication.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); 284 | // 285 | // // validate return URL and redirect back to authorization endpoint 286 | // if (_interaction.IsValidReturnUrl(returnUrl)) 287 | // { 288 | // return Redirect(returnUrl); 289 | // } 290 | // 291 | // return Redirect("~/"); 292 | // } 293 | } 294 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/AccountOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | 7 | namespace IdentityServer.Web.Account 8 | { 9 | public class AccountOptions 10 | { 11 | public static bool AllowLocalLogin = true; 12 | public static bool AllowRememberLogin = true; 13 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 14 | 15 | public static bool ShowLogoutPrompt = true; 16 | public static bool AutomaticRedirectAfterSignOut = false; 17 | 18 | public static bool WindowsAuthenticationEnabled = true; 19 | // specify the Windows authentication schemes you want to use for authentication 20 | public static readonly string[] WindowsAuthenticationSchemes = new string[] { "Negotiate", "NTLM" }; 21 | public static readonly string WindowsAuthenticationDisplayName = "Windows"; 22 | 23 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/AccountService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using IdentityModel; 8 | using IdentityServer4; 9 | using IdentityServer4.Extensions; 10 | using IdentityServer4.Services; 11 | using IdentityServer4.Stores; 12 | using Microsoft.AspNetCore.Http; 13 | 14 | namespace IdentityServer.Web.Account 15 | { 16 | public class AccountService 17 | { 18 | private readonly IClientStore _clientStore; 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly IHttpContextAccessor _httpContextAccessor; 21 | 22 | public AccountService( 23 | IIdentityServerInteractionService interaction, 24 | IHttpContextAccessor httpContextAccessor, 25 | IClientStore clientStore) 26 | { 27 | _interaction = interaction; 28 | _httpContextAccessor = httpContextAccessor; 29 | _clientStore = clientStore; 30 | } 31 | 32 | public async Task BuildLoginViewModelAsync(string returnUrl) 33 | { 34 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 35 | if (context?.IdP != null) 36 | { 37 | // this is meant to short circuit the UI and only trigger the one external IdP 38 | return new LoginViewModel 39 | { 40 | EnableLocalLogin = false, 41 | ReturnUrl = returnUrl, 42 | Username = context?.LoginHint, 43 | ExternalProviders = new ExternalProvider[] {new ExternalProvider { AuthenticationScheme = context.IdP } } 44 | }; 45 | } 46 | 47 | var schemes = _httpContextAccessor.HttpContext.Authentication.GetAuthenticationSchemes(); 48 | 49 | var providers = schemes 50 | .Where(x => x.DisplayName != null && !AccountOptions.WindowsAuthenticationSchemes.Contains(x.AuthenticationScheme)) 51 | .Select(x => new ExternalProvider 52 | { 53 | DisplayName = x.DisplayName, 54 | AuthenticationScheme = x.AuthenticationScheme 55 | }).ToList(); 56 | 57 | if (AccountOptions.WindowsAuthenticationEnabled) 58 | { 59 | // this is needed to handle windows auth schemes 60 | var windowsSchemes = schemes.Where(s => AccountOptions.WindowsAuthenticationSchemes.Contains(s.AuthenticationScheme)); 61 | if (windowsSchemes.Any()) 62 | { 63 | providers.Add(new ExternalProvider 64 | { 65 | AuthenticationScheme = AccountOptions.WindowsAuthenticationSchemes.First(), 66 | DisplayName = AccountOptions.WindowsAuthenticationDisplayName 67 | }); 68 | } 69 | } 70 | 71 | var allowLocal = true; 72 | if (context?.ClientId != null) 73 | { 74 | var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); 75 | if (client != null) 76 | { 77 | allowLocal = client.EnableLocalLogin; 78 | 79 | if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) 80 | { 81 | providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); 82 | } 83 | } 84 | } 85 | 86 | return new LoginViewModel 87 | { 88 | AllowRememberLogin = AccountOptions.AllowRememberLogin, 89 | EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, 90 | ReturnUrl = returnUrl, 91 | Username = context?.LoginHint, 92 | ExternalProviders = providers.ToArray() 93 | }; 94 | } 95 | 96 | public async Task BuildLoginViewModelAsync(LoginInputModel model) 97 | { 98 | var vm = await BuildLoginViewModelAsync(model.ReturnUrl); 99 | vm.Username = model.Username; 100 | vm.RememberLogin = model.RememberLogin; 101 | return vm; 102 | } 103 | 104 | public async Task BuildLogoutViewModelAsync(string logoutId) 105 | { 106 | var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; 107 | 108 | var user = await _httpContextAccessor.HttpContext.GetIdentityServerUserAsync(); 109 | if (user == null || user.Identity.IsAuthenticated == false) 110 | { 111 | // if the user is not authenticated, then just show logged out page 112 | vm.ShowLogoutPrompt = false; 113 | return vm; 114 | } 115 | 116 | var context = await _interaction.GetLogoutContextAsync(logoutId); 117 | if (context?.ShowSignoutPrompt == false) 118 | { 119 | // it's safe to automatically sign-out 120 | vm.ShowLogoutPrompt = false; 121 | return vm; 122 | } 123 | 124 | // show the logout prompt. this prevents attacks where the user 125 | // is automatically signed out by another malicious web page. 126 | return vm; 127 | } 128 | 129 | public async Task BuildLoggedOutViewModelAsync(string logoutId) 130 | { 131 | // get context information (client name, post logout redirect URI and iframe for federated signout) 132 | var logout = await _interaction.GetLogoutContextAsync(logoutId); 133 | 134 | var vm = new LoggedOutViewModel 135 | { 136 | AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, 137 | PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, 138 | ClientName = logout?.ClientId, 139 | SignOutIframeUrl = logout?.SignOutIFrameUrl, 140 | LogoutId = logoutId 141 | }; 142 | 143 | var user = await _httpContextAccessor.HttpContext.GetIdentityServerUserAsync(); 144 | if (user != null) 145 | { 146 | var idp = user.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; 147 | if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider) 148 | { 149 | if (vm.LogoutId == null) 150 | { 151 | // if there's no current logout context, we need to create one 152 | // this captures necessary info from the current logged in user 153 | // before we signout and redirect away to the external IdP for signout 154 | vm.LogoutId = await _interaction.CreateLogoutContextAsync(); 155 | } 156 | 157 | vm.ExternalAuthenticationScheme = idp; 158 | } 159 | } 160 | 161 | return vm; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/ExternalProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Account 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Account 6 | { 7 | public class LoggedOutViewModel 8 | { 9 | public string PostLogoutRedirectUri { get; set; } 10 | public string ClientName { get; set; } 11 | public string SignOutIframeUrl { get; set; } 12 | 13 | public bool AutomaticRedirectAfterSignOut { get; set; } 14 | 15 | public string LogoutId { get; set; } 16 | public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; 17 | public string ExternalAuthenticationScheme { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/LoginInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServer.Web.Account 8 | { 9 | public class LoginInputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | [Required] 14 | public string Password { get; set; } 15 | public bool RememberLogin { get; set; } 16 | public string ReturnUrl { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace IdentityServer.Web.Account 9 | { 10 | public class LoginViewModel : LoginInputModel 11 | { 12 | public bool AllowRememberLogin { get; set; } 13 | public bool EnableLocalLogin { get; set; } 14 | public IEnumerable ExternalProviders { get; set; } 15 | 16 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 17 | } 18 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/LogoutInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Account 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Account/LogoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Account 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ConsentController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Threading.Tasks; 6 | using IdentityServer4.Services; 7 | using IdentityServer4.Stores; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace IdentityServer.Web.Consent 12 | { 13 | /// 14 | /// This controller processes the consent UI 15 | /// 16 | [SecurityHeaders] 17 | public class ConsentController : Controller 18 | { 19 | private readonly ConsentService _consent; 20 | 21 | public ConsentController( 22 | IIdentityServerInteractionService interaction, 23 | IClientStore clientStore, 24 | IResourceStore resourceStore, 25 | ILogger logger) 26 | { 27 | _consent = new ConsentService(interaction, clientStore, resourceStore, logger); 28 | } 29 | 30 | /// 31 | /// Shows the consent screen 32 | /// 33 | /// 34 | /// 35 | [HttpGet] 36 | public async Task Index(string returnUrl) 37 | { 38 | var vm = await _consent.BuildViewModelAsync(returnUrl); 39 | if (vm != null) 40 | { 41 | return View("Index", vm); 42 | } 43 | 44 | return View("Error"); 45 | } 46 | 47 | /// 48 | /// Handles the consent screen postback 49 | /// 50 | [HttpPost] 51 | [ValidateAntiForgeryToken] 52 | public async Task Index(ConsentInputModel model) 53 | { 54 | var result = await _consent.ProcessConsent(model); 55 | 56 | if (result.IsRedirect) 57 | { 58 | return Redirect(result.RedirectUri); 59 | } 60 | 61 | if (result.HasValidationError) 62 | { 63 | ModelState.AddModelError("", result.ValidationError); 64 | } 65 | 66 | if (result.ShowView) 67 | { 68 | return View("Index", result.ViewModel); 69 | } 70 | 71 | return View("Error"); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ConsentInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServer.Web.Consent 8 | { 9 | public class ConsentInputModel 10 | { 11 | public string Button { get; set; } 12 | public IEnumerable ScopesConsented { get; set; } 13 | public bool RememberConsent { get; set; } 14 | public string ReturnUrl { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Consent 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MuchChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ConsentService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using IdentityServer4; 8 | using IdentityServer4.Models; 9 | using IdentityServer4.Services; 10 | using IdentityServer4.Stores; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace IdentityServer.Web.Consent 14 | { 15 | public class ConsentService 16 | { 17 | private readonly IClientStore _clientStore; 18 | private readonly IResourceStore _resourceStore; 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly ILogger _logger; 21 | 22 | public ConsentService( 23 | IIdentityServerInteractionService interaction, 24 | IClientStore clientStore, 25 | IResourceStore resourceStore, 26 | ILogger logger) 27 | { 28 | _interaction = interaction; 29 | _clientStore = clientStore; 30 | _resourceStore = resourceStore; 31 | _logger = logger; 32 | } 33 | 34 | public async Task ProcessConsent(ConsentInputModel model) 35 | { 36 | var result = new ProcessConsentResult(); 37 | 38 | ConsentResponse grantedConsent = null; 39 | 40 | // user clicked 'no' - send back the standard 'access_denied' response 41 | if (model.Button == "no") 42 | { 43 | grantedConsent = ConsentResponse.Denied; 44 | } 45 | // user clicked 'yes' - validate the data 46 | else if (model.Button == "yes" && model != null) 47 | { 48 | // if the user consented to some scope, build the response model 49 | if (model.ScopesConsented != null && model.ScopesConsented.Any()) 50 | { 51 | var scopes = model.ScopesConsented; 52 | if (ConsentOptions.EnableOfflineAccess == false) 53 | { 54 | scopes = scopes.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess); 55 | } 56 | 57 | grantedConsent = new ConsentResponse 58 | { 59 | RememberConsent = model.RememberConsent, 60 | ScopesConsented = scopes.ToArray() 61 | }; 62 | } 63 | else 64 | { 65 | result.ValidationError = ConsentOptions.MuchChooseOneErrorMessage; 66 | } 67 | } 68 | else 69 | { 70 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; 71 | } 72 | 73 | if (grantedConsent != null) 74 | { 75 | // validate return url is still valid 76 | var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); 77 | if (result == null) return result; 78 | 79 | // communicate outcome of consent back to identityserver 80 | await _interaction.GrantConsentAsync(request, grantedConsent); 81 | 82 | // indiate that's it ok to redirect back to authorization endpoint 83 | result.RedirectUri = model.ReturnUrl; 84 | } 85 | else 86 | { 87 | // we need to redisplay the consent UI 88 | result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model); 89 | } 90 | 91 | return result; 92 | } 93 | 94 | public async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null) 95 | { 96 | var request = await _interaction.GetAuthorizationContextAsync(returnUrl); 97 | if (request != null) 98 | { 99 | var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 100 | if (client != null) 101 | { 102 | var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); 103 | if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) 104 | { 105 | return CreateConsentViewModel(model, returnUrl, request, client, resources); 106 | } 107 | else 108 | { 109 | _logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y)); 110 | } 111 | } 112 | else 113 | { 114 | _logger.LogError("Invalid client id: {0}", request.ClientId); 115 | } 116 | } 117 | else 118 | { 119 | _logger.LogError("No consent request matching request: {0}", returnUrl); 120 | } 121 | 122 | return null; 123 | } 124 | 125 | private ConsentViewModel CreateConsentViewModel( 126 | ConsentInputModel model, string returnUrl, 127 | AuthorizationRequest request, 128 | Client client, Resources resources) 129 | { 130 | var vm = new ConsentViewModel(); 131 | vm.RememberConsent = model?.RememberConsent ?? true; 132 | vm.ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(); 133 | 134 | vm.ReturnUrl = returnUrl; 135 | 136 | vm.ClientName = client.ClientName; 137 | vm.ClientUrl = client.ClientUri; 138 | vm.ClientLogoUrl = client.LogoUri; 139 | vm.AllowRememberConsent = client.AllowRememberConsent; 140 | 141 | vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 142 | vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 143 | if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) 144 | { 145 | vm.ResourceScopes = vm.ResourceScopes.Union(new ScopeViewModel[] { 146 | GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) 147 | }); 148 | } 149 | 150 | return vm; 151 | } 152 | 153 | public ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 154 | { 155 | return new ScopeViewModel 156 | { 157 | Name = identity.Name, 158 | DisplayName = identity.DisplayName, 159 | Description = identity.Description, 160 | Emphasize = identity.Emphasize, 161 | Required = identity.Required, 162 | Checked = check || identity.Required, 163 | }; 164 | } 165 | 166 | public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) 167 | { 168 | return new ScopeViewModel 169 | { 170 | Name = scope.Name, 171 | DisplayName = scope.DisplayName, 172 | Description = scope.Description, 173 | Emphasize = scope.Emphasize, 174 | Required = scope.Required, 175 | Checked = check || scope.Required, 176 | }; 177 | } 178 | 179 | private ScopeViewModel GetOfflineAccessScope(bool check) 180 | { 181 | return new ScopeViewModel 182 | { 183 | Name = IdentityServerConstants.StandardScopes.OfflineAccess, 184 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 185 | Description = ConsentOptions.OfflineAccessDescription, 186 | Emphasize = true, 187 | Checked = check 188 | }; 189 | } 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ConsentViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServer.Web.Consent 8 | { 9 | public class ConsentViewModel : ConsentInputModel 10 | { 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public bool AllowRememberConsent { get; set; } 15 | 16 | public IEnumerable IdentityScopes { get; set; } 17 | public IEnumerable ResourceScopes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ProcessConsentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Consent 6 | { 7 | public class ProcessConsentResult 8 | { 9 | public bool IsRedirect => RedirectUri != null; 10 | public string RedirectUri { get; set; } 11 | 12 | public bool ShowView => ViewModel != null; 13 | public ConsentViewModel ViewModel { get; set; } 14 | 15 | public bool HasValidationError => ValidationError != null; 16 | public string ValidationError { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Consent/ScopeViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.Web.Consent 6 | { 7 | public class ScopeViewModel 8 | { 9 | public string Name { get; set; } 10 | public string DisplayName { get; set; } 11 | public string Description { get; set; } 12 | public bool Emphasize { get; set; } 13 | public bool Required { get; set; } 14 | public bool Checked { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IdentityServer/Web/Home/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServer.Web.Home 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorMessage Error { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Threading.Tasks; 6 | using IdentityServer4.Services; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace IdentityServer.Web.Home 10 | { 11 | [SecurityHeaders] 12 | public class HomeController : Controller 13 | { 14 | private readonly IIdentityServerInteractionService _interaction; 15 | 16 | public HomeController(IIdentityServerInteractionService interaction) 17 | { 18 | _interaction = interaction; 19 | } 20 | 21 | public IActionResult Index() 22 | { 23 | return View(); 24 | } 25 | 26 | /// 27 | /// Shows the error page 28 | /// 29 | public async Task Error(string errorId) 30 | { 31 | var vm = new ErrorViewModel(); 32 | 33 | // retrieve error details from identityserver 34 | var message = await _interaction.GetErrorContextAsync(errorId); 35 | if (message != null) 36 | { 37 | vm.Error = message; 38 | } 39 | 40 | return View("Error", vm); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/IdentityServer/Web/SecurityHeadersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | 8 | namespace IdentityServer.Web 9 | { 10 | public class SecurityHeadersAttribute : ActionFilterAttribute 11 | { 12 | public override void OnResultExecuting(ResultExecutingContext context) 13 | { 14 | var result = context.Result; 15 | if (result is ViewResult) 16 | { 17 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) 18 | { 19 | context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); 20 | } 21 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) 22 | { 23 | context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); 24 | } 25 | 26 | var csp = "default-src 'self'"; 27 | // once for standards compliant browsers 28 | if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) 29 | { 30 | context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); 31 | } 32 | // and once again for IE 33 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) 34 | { 35 | context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/IdentityServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "JwtIssuerOptions": { 3 | "Issuer": "http://localhost:5004", 4 | "Audience": "chatapi", 5 | "CertName": "CN=ExampleTest" 6 | } 7 | } -------------------------------------------------------------------------------- /src/IdentityServer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.NETCore.App": { 4 | "version": "1.1.0", 5 | "type": "platform" 6 | }, 7 | "Microsoft.AspNetCore.Diagnostics": "1.1.*", 8 | "Microsoft.AspNetCore.Server.IISIntegration": "1.1.*", 9 | "Microsoft.AspNetCore.Server.Kestrel": "1.1.*", 10 | "Microsoft.Extensions.Logging.Console": "1.1.*", 11 | "Microsoft.AspNetCore.Mvc": "1.1.*", 12 | "Microsoft.AspNetCore.StaticFiles": "1.1.*", 13 | "Microsoft.AspNetCore.Authentication.Google": "1.1.*", 14 | 15 | "IdentityServer4": "1.1.1" 16 | }, 17 | 18 | "tools": { 19 | "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" 20 | }, 21 | 22 | "frameworks": { 23 | "netcoreapp1.1": { 24 | "imports": [ 25 | "dotnet5.6", 26 | "portable-net45+win8" 27 | ] 28 | } 29 | }, 30 | 31 | "buildOptions": { 32 | "emitEntryPoint": true, 33 | "preserveCompilationContext": true 34 | }, 35 | 36 | "runtimeOptions": { 37 | "configProperties": { 38 | "System.GC.Server": true 39 | } 40 | }, 41 | 42 | "publishOptions": { 43 | "include": [ 44 | "wwwroot", 45 | "Views/**/*.cshtml", 46 | "appsettings.json", 47 | "web.config" 48 | ] 49 | }, 50 | 51 | 52 | "scripts": { 53 | "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/IdentityServer/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 65px; 3 | } 4 | .navbar-header { 5 | position: relative; 6 | top: -4px; 7 | } 8 | .navbar-brand > .icon-banner { 9 | position: relative; 10 | top: -2px; 11 | display: inline; 12 | } 13 | .icon { 14 | position: relative; 15 | top: -10px; 16 | } 17 | .logged-out iframe { 18 | display: none; 19 | width: 0; 20 | height: 0; 21 | } 22 | .page-consent .client-logo { 23 | float: left; 24 | } 25 | .page-consent .client-logo img { 26 | width: 80px; 27 | height: 80px; 28 | } 29 | .page-consent .consent-buttons { 30 | margin-top: 25px; 31 | } 32 | .page-consent .consent-form .consent-scopecheck { 33 | display: inline-block; 34 | margin-right: 5px; 35 | } 36 | .page-consent .consent-form .consent-description { 37 | margin-left: 25px; 38 | } 39 | .page-consent .consent-form .consent-description label { 40 | font-weight: normal; 41 | } 42 | .page-consent .consent-form .consent-remember { 43 | padding-left: 16px; 44 | } -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/css/site.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 65px; 3 | } 4 | 5 | .navbar-header { 6 | position:relative; 7 | top:-4px; 8 | } 9 | 10 | .navbar-brand > .icon-banner { 11 | position:relative; 12 | top:-2px; 13 | display:inline; 14 | } 15 | 16 | .icon { 17 | position:relative; 18 | top:-10px; 19 | } 20 | 21 | .logged-out iframe { 22 | display:none; 23 | width:0; 24 | height:0; 25 | } 26 | 27 | .page-consent { 28 | .client-logo { 29 | float: left; 30 | 31 | img { 32 | width: 80px; 33 | height: 80px; 34 | } 35 | } 36 | 37 | .consent-buttons { 38 | margin-top: 25px; 39 | } 40 | 41 | .consent-form { 42 | .consent-scopecheck { 43 | display: inline-block; 44 | margin-right: 5px; 45 | } 46 | 47 | .consent-scopecheck[disabled] { 48 | //visibility:hidden; 49 | } 50 | 51 | .consent-description { 52 | margin-left: 25px; 53 | 54 | label { 55 | font-weight: normal; 56 | } 57 | } 58 | 59 | .consent-remember { 60 | padding-left: 16px; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{margin-top:65px;}.navbar-header{position:relative;top:-4px;}.navbar-brand>.icon-banner{position:relative;top:-2px;display:inline;}.icon{position:relative;top:-10px;}.logged-out iframe{display:none;width:0;height:0;}.page-consent .client-logo{float:left;}.page-consent .client-logo img{width:80px;height:80px;}.page-consent .consent-buttons{margin-top:25px;}.page-consent .consent-form .consent-scopecheck{display:inline-block;margin-right:5px;}.page-consent .consent-form .consent-description{margin-left:25px;}.page-consent .consent-form .consent-description label{font-weight:normal;}.page-consent .consent-form .consent-remember{padding-left:16px;} -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/icon.jpg -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/icon.png -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/js/signout-redirect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | var a = document.querySelector("a.PostLogoutRedirectUri"); 3 | if (a) { 4 | window.location = a.href; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/IdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/Web/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | npm -------------------------------------------------------------------------------- /src/Web/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /src/Web/.idea/Web.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /src/Web/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Web/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /src/Web/.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Web/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 1.8 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Web/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Web/.idea/typescript-compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/Web/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:boron 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | COPY ./package.json /usr/src/app/ 7 | RUN npm install 8 | 9 | COPY . /usr/src/app 10 | 11 | EXPOSE 3000 12 | 13 | CMD [ "npm", "start" ] 14 | -------------------------------------------------------------------------------- /src/Web/Web.njsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | {bd437718-9cb1-4820-a89d-79d58c5495c5} 7 | 8 | ShowAllFiles 9 | 10 | 11 | . 12 | . 13 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 14 | true 15 | CommonJS 16 | true 17 | true 18 | 11.0 19 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 20 | Web 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | False 70 | True 71 | 0 72 | / 73 | http://localhost:48022/ 74 | False 75 | True 76 | http://localhost:1337 77 | False 78 | 79 | 80 | 81 | 82 | 83 | 84 | CurrentPage 85 | True 86 | False 87 | False 88 | False 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | False 98 | False 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/Web/azure/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts-ts": "1.1.6" 7 | }, 8 | "dependencies": { 9 | "@types/jest": "^18.1.1", 10 | "@types/node": "^7.0.5", 11 | "@types/react": "^15.0.10", 12 | "@types/react-dom": "^0.14.23", 13 | "@types/immutability-helper": "*", 14 | "@types/jquery": "*", 15 | "@types/react-redux": "*", 16 | "@types/react-router": "^2.0.44", 17 | "@types/redux-actions": "1.2.0", 18 | "@types/shortid": "0.0.28", 19 | "@types/signalr": "2.2.33", 20 | "babel-polyfill": "^6.16.0", 21 | "classnames": "2.2.5", 22 | "react": "^15.4.2", 23 | "react-dom": "^15.4.2", 24 | "react-redux": "^4.4.6", 25 | "react-router": "^3.0.0", 26 | "redux-observable": "0.12.2", 27 | "rxjs": "5.0.3", 28 | "immutability-helper": "2.1.1", 29 | "signalr": "^2.2.1", 30 | "expose-loader": "0.7.1", 31 | "tachyons": "^4.6.2", 32 | "jquery": "3.1.1", 33 | "shortid": "2.2.6", 34 | "oidc-client": "^1.2.2" 35 | }, 36 | "scripts": { 37 | "start": "react-scripts-ts start", 38 | "build": "react-scripts-ts build", 39 | "test": "react-scripts-ts test --env=jsdom", 40 | "eject": "react-scripts-ts eject" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebridge/IdentityServer4SignalR/52aafbcc29ac514b7806f67688eb99c8495bec41/src/Web/public/favicon.ico -------------------------------------------------------------------------------- /src/Web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | React App 19 | 20 | 21 |
    22 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Web/public/pace/dataurl.css: -------------------------------------------------------------------------------- 1 | .pace { 2 | -webkit-pointer-events: none; 3 | pointer-events: none; 4 | -webkit-user-select: none; 5 | -moz-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | .pace-inactive { 10 | display: none; 11 | } 12 | 13 | .pace .pace-progress { 14 | background: #29d; 15 | position: fixed; 16 | z-index: 2000; 17 | top: 0; 18 | right: 100%; 19 | width: 100%; 20 | height: 2px; 21 | } 22 | 23 | .pace .pace-progress-inner { 24 | display: block; 25 | position: absolute; 26 | right: 0px; 27 | width: 100px; 28 | height: 100%; 29 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 30 | opacity: 1.0; 31 | -webkit-transform: rotate(3deg) translate(0px, -4px); 32 | -moz-transform: rotate(3deg) translate(0px, -4px); 33 | -ms-transform: rotate(3deg) translate(0px, -4px); 34 | -o-transform: rotate(3deg) translate(0px, -4px); 35 | transform: rotate(3deg) translate(0px, -4px); 36 | } 37 | 38 | .pace .pace-activity { 39 | display: block; 40 | position: fixed; 41 | z-index: 2000; 42 | top: 15px; 43 | right: 15px; 44 | width: 14px; 45 | height: 14px; 46 | border: solid 2px transparent; 47 | border-top-color: #29d; 48 | border-left-color: #29d; 49 | border-radius: 10px; 50 | -webkit-animation: pace-spinner 400ms linear infinite; 51 | -moz-animation: pace-spinner 400ms linear infinite; 52 | -ms-animation: pace-spinner 400ms linear infinite; 53 | -o-animation: pace-spinner 400ms linear infinite; 54 | animation: pace-spinner 400ms linear infinite; 55 | } 56 | 57 | @-webkit-keyframes pace-spinner { 58 | 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 59 | 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } 60 | } 61 | @-moz-keyframes pace-spinner { 62 | 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } 63 | 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } 64 | } 65 | @-o-keyframes pace-spinner { 66 | 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } 67 | 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } 68 | } 69 | @-ms-keyframes pace-spinner { 70 | 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } 71 | 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } 72 | } 73 | @keyframes pace-spinner { 74 | 0% { transform: rotate(0deg); transform: rotate(0deg); } 75 | 100% { transform: rotate(360deg); transform: rotate(360deg); } 76 | } 77 | -------------------------------------------------------------------------------- /src/Web/public/pace/pace.min.js: -------------------------------------------------------------------------------- 1 | /*! pace 1.0.0 */ 2 | (function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
    \n
    \n
    \n
    ',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e,f;f=[];for(d in b.prototype)try{e=b.prototype[d],f.push(null==a[d]&&"function"!=typeof e?a[d]=e:void 0)}catch(g){c=g}return f},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); -------------------------------------------------------------------------------- /src/Web/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactRouter from "react-router"; 3 | import * as ReactRedux from "react-redux"; 4 | import "./app.css"; 5 | import Error404 from "./errors/error404"; 6 | import LoginCallback from "./login/loginCallback"; 7 | import Echo from "./echo/echo"; 8 | import Home from "./home"; 9 | import MainLayout from "./layout/mainLayout"; 10 | import createStoreWithInitialState from "./store"; 11 | 12 | interface IAppProps { 13 | initialState: any; 14 | } 15 | 16 | class App extends React.Component { 17 | render() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /src/Web/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {withAuthToken} from "./redux-oidc/withAuthToken"; 3 | 4 | import {IWithAuthChildComponentProps} from "./redux-oidc/withAuthToken"; 5 | 6 | class Home extends React.Component { 7 | 8 | public render(): JSX.Element { 9 | 10 | const btnClass = "no-underline f6 tc db w-100 pv3 bg-animate bg-blue hover-bg-dark-blue white br2"; 11 | const { valid_identity_token, valid_access_token } = this.props; 12 | if (valid_identity_token && valid_access_token) { 13 | return ( 14 |
    15 |
    16 | 17 |
    18 |
    19 |
    20 |
    21 |

    JWT Info

    22 |

    23 | View your tokens on JWT.io 24 |

    25 |
    26 |
    27 |
    28 | Access Token 31 |
    32 |
    33 | Identity Token 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 | ); 43 | } else { 44 | return(
    Please log in!
    ); 45 | } 46 | } 47 | } 48 | 49 | export default withAuthToken(Home); 50 | -------------------------------------------------------------------------------- /src/Web/src/config/env.ts: -------------------------------------------------------------------------------- 1 | export enum Mode { 2 | Production, 3 | Development 4 | } 5 | 6 | const currentMode = process.env.mode == null || 7 | process.env.mode.toLowerCase() === "dev" ? Mode.Development : Mode.Production; 8 | 9 | export const globalConfig = { 10 | currentMode, 11 | baseIdentityUrl: currentMode === Mode.Development ? "http://localhost:5004" : "https://auth.example.org", 12 | baseChatApiUrl: currentMode === Mode.Development ? "http://localhost:5000" : "https://chatapi.example.org", 13 | baseApiUrl: currentMode === Mode.Development ? "http://localhost:5001" : "https://api.example.org", 14 | currentBaseUrl: `${window.location.protocol}//${window.location.hostname}${window.location.port ? 15 | `:${window.location.port}` : ""}` 16 | }; 17 | -------------------------------------------------------------------------------- /src/Web/src/config/oidcConfig.ts: -------------------------------------------------------------------------------- 1 | import { globalConfig } from "./env"; 2 | 3 | const authUrl = globalConfig.baseIdentityUrl; 4 | const baseUrl = globalConfig.currentBaseUrl; 5 | 6 | export const oidcImplicitSettings = { 7 | // URL of your OpenID Connect server. 8 | // The library uses it to access the metadata document 9 | 10 | // authority points to our IdentityServer 11 | authority: authUrl, 12 | 13 | // the client_id is an arbitrary string that matches our IdentityServer4 ClientId 14 | client_id: "js.implicit", 15 | 16 | // after the user has authenticated with IdentityServer, he will get redirected 17 | // to our callback page. 18 | redirect_uri: `${baseUrl}/callback`, 19 | 20 | post_logout_redirect_uri: `${baseUrl}/index`, 21 | 22 | // For JavaScript clients, we want both an identity token and an 23 | // access token as per Openid Connect. 24 | response_type: "id_token token", 25 | 26 | // Resources requested during the authorisation request 27 | scope: "openid email profile chatapi", 28 | 29 | // Number of seconds before the token expires to trigger 30 | // the `tokenExpiring` event 31 | accessTokenExpiringNotificationTime: 4, 32 | 33 | // Do we want to renew the access token automatically when it's 34 | // about to expire? 35 | automaticSilentRenew: true, 36 | 37 | // Do we want to filter OIDC protocol-specific claims from the response? 38 | filterProtocolClaims: true, 39 | 40 | nonce : "N" + Math.random() + "" + Date.now() 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /src/Web/src/config/triggers.ts: -------------------------------------------------------------------------------- 1 | import * as flashActions from "../flash/flashActions"; 2 | import "rxjs/Rx"; 3 | import * as ReduxObservable from "redux-observable"; 4 | import * as oidcConstants from "../redux-oidc/oidcConstants"; 5 | 6 | // todo: change any to IFlashFailure or something 7 | const flashNotifications = (action$: ReduxObservable.ActionsObservable, store) => { 8 | return action$.ofType(oidcConstants.LOGIN_NETWORK_FAILED) 9 | // maybe move the IAjaxFailed to a separate file 10 | .do(() => console.log("TRIGGERING ACTION")) 11 | .map((action) => { 12 | return flashActions.flashActionCreators.addFlashErrorMessage(action.message); 13 | }); 14 | }; 15 | 16 | 17 | export default ReduxObservable.combineEpics(flashNotifications); 18 | -------------------------------------------------------------------------------- /src/Web/src/echo/echo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import EchoDialogue from "./echoDialog"; 4 | 5 | import { globalConfig } from "../config/env"; 6 | 7 | const Echo = (props) => ( 8 | 13 | ); 14 | 15 | export default Echo; 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Web/src/echo/echoConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | //import FlashConnector from "../flash/flashConnector"; 3 | import {withSignalR, ISignalRConnectorProps} from "../react-signalr/signalrConnector"; 4 | import {withAuthToken} from "../redux-oidc/withAuthToken"; 5 | import {IEchoMessage} from "./echoConstants"; 6 | 7 | 8 | interface IEchoConnectorState { 9 | messages: IEchoMessage[]; 10 | } 11 | 12 | /* 13 | * a HOC that handles the Echo functions in SignalR 14 | */ 15 | 16 | export const withEchoConnector = (ConnectedComponent: typeof React.Component, 17 | DisconnectedComponent: React.ReactType) => { 18 | 19 | 20 | class WrappedChatConnector extends React.Component { 22 | 23 | 24 | public constructor(props: ISignalRConnectorProps) { 25 | super(props); 26 | this.state = this.state || {messages: []}; 27 | } 28 | 29 | componentWillMount() { 30 | this.props.openConnection(); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.props.closeConnection(); 35 | } 36 | 37 | // send a message from client to server 38 | public sendMessage = (message: String) => { 39 | // console.log("echoConnector.sendMessage", message); 40 | this.props.invoker("sendMessage", message); 41 | } 42 | 43 | // server-to-client message handlers. 44 | // this is initialized by passing the name of the signalR client-side javascript 45 | // function (i.e. "echoMessage") that we want to listen for into the "clientmethods" 46 | // attribute of SignalRConnector. 47 | public echoMessage(message: string) { 48 | this.setState(Object.assign({}, this.state, {messages: [...this.state.messages, message]})); 49 | } 50 | 51 | public render(): JSX.Element { 52 | const childProps = { 53 | sendMessageToServer: this.sendMessage, 54 | messages: this.state.messages 55 | }; 56 | //const errors = [this.props.connectionError, this.props.invocationError] 57 | // .filter(x => x !== null && x !== undefined); 58 | //console.log(">>> echoConnector RENDERS ", childProps); 59 | 60 | return ( 61 | (this.props.isConnected 62 | ? 63 | : ) 64 | ); 65 | } 66 | 67 | } 68 | 69 | return withAuthToken(withSignalR(WrappedChatConnector)); 70 | }; 71 | 72 | export default withEchoConnector; 73 | 74 | -------------------------------------------------------------------------------- /src/Web/src/echo/echoConstants.ts: -------------------------------------------------------------------------------- 1 | export interface IEchoMessage { 2 | message: string; 3 | fullName?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/Web/src/echo/echoDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as classnames from "classnames"; 3 | import * as update from "immutability-helper"; 4 | //import "expose-loader?jQuery!jquery"; 5 | //import "expose-loader?$!jquery"; 6 | import * as $ from "jquery"; 7 | (window as any).jQuery = $; // hack to get SignalR to find it 8 | import "signalr"; 9 | 10 | import MessageList from "./messageList"; 11 | import {withEchoConnector} from "./echoConnector"; 12 | import NotConnectedToSignalR from "./notConnectedToSignalR"; 13 | import {IEchoMessage} from "./echoConstants"; 14 | 15 | // the props that come from react 16 | interface IEchoDispatchProps { 17 | sendMessageToServer: (message: String) => Promise; 18 | isConnectedToServer: boolean; 19 | } 20 | 21 | interface IEchoOwnProps { 22 | messages: IEchoMessage[]; 23 | } 24 | 25 | interface IEchoState { 26 | message: string; 27 | } 28 | 29 | const initialState = { 30 | message: "", 31 | }; 32 | 33 | class EchoDialog extends React.Component { 34 | 35 | constructor(props: IEchoDispatchProps & IEchoOwnProps) { 36 | super(props); 37 | this.state = this.state || initialState; 38 | } 39 | 40 | onSendClick(e: React.MouseEvent) { 41 | e.preventDefault(); 42 | this.props.sendMessageToServer(this.state.message); 43 | //this.invokeHubMethod("sendMessage", this.state.message); 44 | } 45 | 46 | handleChange(e: React.FormEvent) { 47 | const {name, value} = e.target as HTMLInputElement; // why is the cast required? 48 | const diff = { [name]: {$set: value} }; 49 | this.setState(update(this.state, diff)); 50 | } 51 | 52 | 53 | render() { 54 | const btnTextColor = "white"; 55 | const btnBgColor = "bg-black-70"; 56 | const btnBgHoverColor = "hover-bg-black"; 57 | const style = {}; 58 | const disabled = false; 59 | //console.log("MESSAGES", this.props.messages); 60 | /*const btnTextColor = this.state.connectionid ? "white" : "lightGrey";*/ 61 | // const btnBgColor = this.state.connectionid ? "bg-black-70" : "bg-grey"; 62 | // const btnBgHoverColor = this.state.connectionid ? "hover-bg-black" : "light-silver"; 63 | // const style = this.state.connectionid ? {} : {cursor: "progress"} ; 64 | // const disabled = !this.state.connectionid; 65 | //console.log(classnames("test")); 66 | return ( 67 | 68 |
    69 |
    70 |
    71 | Send via SignalR 72 | 73 |
    74 | 75 | 85 | 94 |
    95 |
    96 |
    97 |
    98 | 99 |
    100 |
    101 | ); 102 | } 103 | 104 | } 105 | 106 | export default withEchoConnector(EchoDialog, NotConnectedToSignalR); 107 | 108 | -------------------------------------------------------------------------------- /src/Web/src/echo/messageList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {IEchoMessage} from "./echoConstants"; 3 | 4 | interface IMessageProps { 5 | message: IEchoMessage; 6 | } 7 | 8 | interface IMessageListProps { 9 | messages: IEchoMessage[]; 10 | } 11 | 12 | const Message = (props: IMessageProps) =>
  • 13 | {props.message.fullName}: {props.message.message}
  • ; 14 | 15 | const MessageList = (props: IMessageListProps) => { 16 | const messages = props.messages.map((message, index) => ); 17 | return ( 18 |
    19 |
      20 | {messages} 21 |
    22 |
    23 | ); 24 | }; 25 | 26 | export default MessageList; -------------------------------------------------------------------------------- /src/Web/src/echo/notAuthenticated.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const NotAuthenticated = (props: any) =>
    Please log in to use the Echo Server.
    ; 4 | 5 | export default NotAuthenticated; 6 | 7 | -------------------------------------------------------------------------------- /src/Web/src/echo/notConnectedToSignalR.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const NotConnectedToSignalR = (props: any) =>
    Unable to access the echo server.
    ; 4 | 5 | export default NotConnectedToSignalR; 6 | 7 | -------------------------------------------------------------------------------- /src/Web/src/epics.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics } from "redux-observable"; 2 | import triggerEpics from "./config/triggers"; 3 | 4 | export default combineEpics( 5 | triggerEpics 6 | ); 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Web/src/errors/error404.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "tachyons"; 3 | 4 | class Error404 extends React.Component<{}, {}> { 5 | public render(): JSX.Element { 6 | return ( 7 |
    8 |
    9 |
    10 |

    404

    11 |

    That page is MISSING!

    12 |
    13 |

    Are you looking for the Chat page?

    14 |
    15 |
    16 | ); 17 | } 18 | } 19 | 20 | export default Error404; 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Web/src/flash/flash.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactRedux from "react-redux"; 3 | import { IFlashMessage, IFlashProps } from "./flashConstants"; 4 | import { FlashMessage } from "./flashMessage"; 5 | import { deleteFlashMessage } from "./flashActions"; 6 | 7 | class Flash extends React.Component { 8 | 9 | public static propTypes: React.ValidationMap = { 10 | messages: React.PropTypes.any.isRequired 11 | }; 12 | 13 | public render(): JSX.Element { 14 | const messageComponents = 15 | this.props.messages.map((m: IFlashMessage) => ( 16 | 17 | )); 18 | return ( 19 |
    {messageComponents}
    20 | ); 21 | }; 22 | 23 | } 24 | 25 | 26 | const mapStateToProps: any = state => { 27 | return {messages: state.flash}; 28 | }; 29 | 30 | 31 | export default ReactRedux.connect(mapStateToProps, { deleteFlashMessage })( Flash ); 32 | 33 | -------------------------------------------------------------------------------- /src/Web/src/flash/flashActions.ts: -------------------------------------------------------------------------------- 1 | import { ADD_FLASH_MESSAGE, DELETE_FLASH_MESSAGE, DELETE_ALL_FLASH_MESSAGES, IFlashMessage } from "./flashConstants"; 2 | 3 | export function addFlashMessage(message: IFlashMessage) { 4 | return { 5 | type: ADD_FLASH_MESSAGE, 6 | message 7 | }; 8 | } 9 | 10 | export function deleteFlashMessage(id: string) { 11 | return { 12 | type: DELETE_FLASH_MESSAGE, 13 | id 14 | }; 15 | } 16 | 17 | export function deleteAllFlashMessages() { 18 | return { 19 | type: DELETE_ALL_FLASH_MESSAGES 20 | }; 21 | } 22 | 23 | const addFlashSuccessMessage = (text: string) => addFlashMessage({text, type: "success"}); 24 | 25 | const addFlashErrorMessage = (text: string) => addFlashMessage({text, type: "error"}); 26 | 27 | export const flashActionCreators: any = { 28 | addFlashMessage, 29 | addFlashSuccessMessage, 30 | addFlashErrorMessage, 31 | deleteAllFlashMessages 32 | }; 33 | -------------------------------------------------------------------------------- /src/Web/src/flash/flashConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {IFlashableDispatchProps, withFlash} from "./withFlash"; 3 | 4 | interface IFlashConnectorProps { 5 | errors?: string[]; 6 | successes?: string[]; 7 | } 8 | 9 | // this class is to work around the issue where HOC-wrapped classes 10 | // aren't accessible (easily) via ref. This class can be used as a child 11 | // of the class that normally would be wrapped, and the properties passed through. 12 | // https://facebook.github.io/react/docs/higher-order-components.html#refs-arent-passed-through 13 | 14 | class FlashConnector extends React.Component { 15 | 16 | componentWillReceiveProps(nextProps: IFlashConnectorProps & IFlashableDispatchProps) { 17 | 18 | if (nextProps.errors && nextProps.errors !== this.props.errors) { 19 | this.props.errors.map(err => 20 | this.props.flashActions.addFlashErrorMessage(err) 21 | ); 22 | } 23 | if (nextProps.successes && nextProps.successes !== this.props.successes) { 24 | this.props.errors.map(s => 25 | this.props.flashActions.addFlashSuccessMessage(s) 26 | ); 27 | } 28 | 29 | } 30 | 31 | public render(): JSX.Element { 32 | return null; 33 | }; 34 | } 35 | 36 | export default withFlash(FlashConnector); 37 | -------------------------------------------------------------------------------- /src/Web/src/flash/flashConstants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const ADD_FLASH_MESSAGE = "flash/ADD_MESSAGE"; 3 | 4 | export const DELETE_FLASH_MESSAGE = "flash/DELETE_MESSAGE"; 5 | 6 | export const DELETE_ALL_FLASH_MESSAGES = "flash/DELETE_ALL_MESSAGES"; 7 | 8 | 9 | export interface IFlashMessage { 10 | id?: string; 11 | text: string; 12 | type: string; 13 | } 14 | 15 | export interface IFlashMessageProps { 16 | message: IFlashMessage; 17 | deleteFlashMessage: (id: String) => void; 18 | } 19 | 20 | export interface IFlashProps { 21 | messages?: IFlashMessage[]; 22 | deleteFlashMessage: (id: String) => void; 23 | } 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Web/src/flash/flashMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IFlashMessageProps } from "./flashConstants"; 3 | //import * as classnames from "classnames"; 4 | 5 | export class FlashMessage extends React.Component { 6 | 7 | public static propTypes: React.ValidationMap = { 8 | message: React.PropTypes.object.isRequired, 9 | deleteFlashMessage: React.PropTypes.func.isRequired 10 | }; 11 | 12 | public onClick(e: MouseEvent): void { 13 | this.props.deleteFlashMessage(this.props.message.id); 14 | } 15 | 16 | public render(): JSX.Element { 17 | //const fg = this.props.color || "red"; 18 | const icon = "M16 0 A16 16 0 0 1 16 32 A16 16 0 0 1 16 0 M19 15 " + 19 | "L13 15 L13 26 L19 26 z M16 6 A3 3 0 0 0 16 12 A3 3 0 0 0 16 6"; 20 | const { type, text } = this.props.message; 21 | const fg = type === "success" ? "green" : "red"; 22 | 23 | return ( 24 |
    25 | 26 | info icon 27 | 28 | 29 |
    30 | 35 |
    36 | {text} 37 | 38 |
    39 | 40 | 41 | ); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Web/src/flash/flashReducers.ts: -------------------------------------------------------------------------------- 1 | import { ADD_FLASH_MESSAGE, DELETE_FLASH_MESSAGE, DELETE_ALL_FLASH_MESSAGES } from "./flashConstants"; 2 | import * as shortid from "shortid"; 3 | 4 | export default (state = [], action: any) => { 5 | 6 | switch (action.type) { 7 | case ADD_FLASH_MESSAGE: 8 | return [ 9 | ...state, 10 | { 11 | id: shortid.generate(), 12 | type: action.message.type, 13 | text: action.message.text 14 | } 15 | ]; 16 | case DELETE_FLASH_MESSAGE: 17 | const index = state.findIndex((m: any) => m.id === action.id); 18 | if (index >= 0) { 19 | return [ 20 | ...state.slice(0, index), 21 | ...state.slice(index + 1) 22 | ]; 23 | } 24 | return state; 25 | case DELETE_ALL_FLASH_MESSAGES: 26 | return []; 27 | 28 | default: return state; 29 | } 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /src/Web/src/flash/withFlash.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as Redux from "redux"; 3 | import * as ReactRedux from "react-redux"; 4 | import { flashActionCreators } from "./flashActions"; 5 | 6 | /* 7 | * Add the ability to use the flash message to a child component 8 | * without that child component needing to know about Redux. 9 | * 10 | * via "Props Proxy" HOC pattern (see 11 | * https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#b547) 12 | * 13 | * Usage: 14 | * 15 | * import { withFlash } from "../flash/flashable"; 16 | * 17 | * class MyUnwrappedComponent extends React.Component { 18 | * 19 | * public myFunction(): void { 20 | * this.props.flashActions.addFlashErrorMessage("My Error Message"); 21 | * } 22 | * } 23 | * 24 | * export const MyComponent = withFlash(MyUnwrappedComponent); 25 | * 26 | */ 27 | 28 | export interface IFlashableDispatchProps { 29 | flashActions: any; 30 | } 31 | 32 | export const withFlash = (Component: typeof React.Component) => { 33 | 34 | class Flashable extends React.Component { 35 | 36 | //private childRef: React.Component = null; 37 | 38 | public render(): JSX.Element { 39 | //return { this.childRef = c; }} {...this.props} />; 40 | return ; 41 | } 42 | } 43 | 44 | const mapDispatchToProps: any = (dispatch): IFlashableDispatchProps => ({ 45 | flashActions: Redux.bindActionCreators(flashActionCreators, dispatch) 46 | }); 47 | 48 | return ReactRedux.connect(null, mapDispatchToProps)(Flashable) as React.ComponentClass; 49 | }; 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/Web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import "./index.css"; 5 | import {oidcGetInitialLoginState} from "./redux-oidc/oidcMiddleware"; 6 | import App from "./app"; 7 | 8 | oidcGetInitialLoginState().then( 9 | state => { 10 | ReactDOM.render( 11 | , 12 | document.getElementById("app") as HTMLElement 13 | ); 14 | 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /src/Web/src/layout/mainLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link} from "react-router"; 3 | import * as classnames from "classnames"; 4 | 5 | import Flash from "../flash/flash"; 6 | import LoginLink from "../login/loginLink"; 7 | import LogoutLink from "../login/logoutLink"; 8 | 9 | class MainLayout extends React.Component<{}, {}> { 10 | public render(): JSX.Element { 11 | const linkClasses = classnames("f6", "f5-l", "link", "bg-animate", 12 | "black-80", "hover-bg-lightest-blue", 13 | "dib", "pa3", "ph4-l"); 14 | return ( 15 |
    16 | 17 |
    18 |

    OpenID Connect / JWT and SignalR Example

    19 | 25 |
    26 | 27 |
    28 |
    29 | {this.props.children} 30 |
    31 |
    32 |
    33 | ); 34 | } 35 | } 36 | 37 | export default MainLayout; 38 | -------------------------------------------------------------------------------- /src/Web/src/login/authComponentWrappers.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as Redux from "redux"; 3 | import * as ReactRedux from "react-redux"; 4 | 5 | interface IWhenLoggedInProps { 6 | is_authenticated: boolean; 7 | dispatch: Redux.Dispatch; 8 | } 9 | 10 | const whenLoggedInOrOut = (Component, showWhenLoggedIn: boolean) => { 11 | 12 | class WhenLoggedIn extends React.Component { 13 | 14 | public static propTypes: React.ValidationMap = { 15 | is_authenticated: React.PropTypes.bool.isRequired 16 | }; 17 | 18 | public render(): JSX.Element { 19 | const { is_authenticated, dispatch, ...rest } = this.props; 20 | 21 | return is_authenticated === showWhenLoggedIn ? : ; 22 | } 23 | 24 | } 25 | 26 | const mapStateToProps = (state) => ( 27 | state.oidc 28 | ? { is_authenticated: state.oidc.is_authenticated && !state.oidc.expired } 29 | : { is_authenticated: false} 30 | ); 31 | 32 | 33 | return ReactRedux.connect(mapStateToProps, null)(WhenLoggedIn); //as React.ComponentClass; 34 | }; 35 | 36 | export const whenLoggedIn = (Component): React.ComponentClass => whenLoggedInOrOut(Component, true); 37 | 38 | export const whenLoggedOut = (Component): React.ComponentClass => whenLoggedInOrOut(Component, false); 39 | 40 | // export const WhenLoggedIn = ({component: Component, children, ...props}) => { 41 | // return whenLoggedInOrOut({children}, true) as React.ComponentClass; 42 | // }; 43 | -------------------------------------------------------------------------------- /src/Web/src/login/loginCallback.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {withOidcActions, IOidcDispatchProps} from "../redux-oidc/oidcComponentWrapper"; 4 | 5 | interface ILoginCallbackOwnProps extends React.HTMLAttributes {} 6 | 7 | class LoginCallbackUnwrapped extends React.Component { 8 | 9 | public componentDidMount() { 10 | this.completeSignup(); 11 | } 12 | 13 | public completeSignup() { 14 | this.props.oidcActions.newLoginCallbackRequest(); 15 | } 16 | 17 | public render(): JSX.Element { 18 | return ( 19 |
    Signing in...
    20 | ); 21 | } 22 | 23 | } 24 | 25 | const LoginCallback = withOidcActions(LoginCallbackUnwrapped); 26 | 27 | export default LoginCallback; -------------------------------------------------------------------------------- /src/Web/src/login/loginLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {IOidcDispatchProps, withOidcActions} from "../redux-oidc/oidcComponentWrapper"; 3 | import {whenLoggedOut} from "./authComponentWrappers"; 4 | 5 | interface ILoginLinkOwnProps extends React.HTMLAttributes {} 6 | 7 | class LoginLinkUnwrapped extends React.Component { 8 | 9 | public static contextTypes: React.ValidationMap = { 10 | router: React.PropTypes.object.isRequired 11 | }; 12 | 13 | public onLoginClick(e: MouseEvent) { 14 | e.preventDefault(); 15 | // get the current route so we know how to get back to this page after login 16 | const currentRoute = this.context.router.location.pathname; 17 | this.props.oidcActions.loginRequest(currentRoute); 18 | } 19 | 20 | public render(): JSX.Element { 21 | // TODO: is there a way to fix that cast? 22 | const { oidcActions, ...rest } = this.props; 23 | return ( 24 | Login 27 | ); 28 | } 29 | 30 | } 31 | 32 | const LoginLink = whenLoggedOut(withOidcActions(LoginLinkUnwrapped)); 33 | 34 | export default LoginLink; 35 | -------------------------------------------------------------------------------- /src/Web/src/login/logoutLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {IOidcDispatchProps, withOidcActions} from "../redux-oidc/oidcComponentWrapper"; 3 | import {whenLoggedIn} from "./authComponentWrappers"; 4 | 5 | interface ILogoutLinkOwnProps extends React.HTMLAttributes {} 6 | 7 | class LogoutLinkUnwrapped extends React.Component { 8 | 9 | public static contextTypes: React.ValidationMap = { 10 | router: React.PropTypes.object.isRequired 11 | }; 12 | 13 | public onLogoutClick(e: MouseEvent) { 14 | e.preventDefault(); 15 | this.props.oidcActions.logoutRequest(); 16 | } 17 | 18 | public render(): JSX.Element { 19 | // TODO: is there a way to fix that cast? 20 | const { oidcActions, ...rest } = this.props; 21 | return ( 22 | Logout 25 | ); 26 | } 27 | 28 | } 29 | 30 | const LogoutLink = whenLoggedIn(withOidcActions(LogoutLinkUnwrapped)); 31 | 32 | export default LogoutLink; 33 | -------------------------------------------------------------------------------- /src/Web/src/react-signalr/signalrConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as $ from "jquery"; 3 | (window as any).jQuery = $; // hack to get SignalR to find it 4 | import "signalr"; 5 | 6 | import {IWithAuthChildComponentProps} from "../redux-oidc/withAuthToken"; 7 | import {withFlash, IFlashableDispatchProps} from "../flash/withFlash"; 8 | 9 | export interface ISignalRViewProps { 10 | hub: string; 11 | url: string; 12 | logging?: boolean; 13 | clientmethods?: string[]; 14 | errorHandler?: (err: string) => void; 15 | } 16 | 17 | // props that the child connector will receive 18 | // from the parent SignalRConnector. 19 | 20 | export interface ISignalRConnectorProps { 21 | invoker: (method: string, ...args: any[]) => void; 22 | isConnected: boolean; 23 | openConnection: () => void; // test use only 24 | closeConnection: () => void; // test use only 25 | } 26 | 27 | export interface ISignalRState { 28 | isConnected: boolean; 29 | } 30 | 31 | const initialState: ISignalRState = { 32 | isConnected: false 33 | }; 34 | 35 | // TODO: make this Component a type that implements the 36 | // props we need to inject. 37 | export const withSignalR = (Component: typeof React.Component) => { 38 | 39 | class SignalRConnector extends React.Component { 41 | 42 | public static propTypes: React.ValidationMap = { 43 | hub: React.PropTypes.string.isRequired, 44 | url: React.PropTypes.string.isRequired, 45 | logging: React.PropTypes.bool, 46 | clientmethods: React.PropTypes.arrayOf(React.PropTypes.string), 47 | requireauthtoken: React.PropTypes.bool, 48 | valid_access_token: React.PropTypes.string 49 | }; 50 | 51 | private authToken?: string = null; 52 | 53 | private wrappedComponentRef: React.Component = null; 54 | 55 | private hubConnection: SignalR.Hub.Connection = null; 56 | 57 | private proxy: SignalR.Hub.Proxy = null; 58 | 59 | public constructor(props: ISignalRViewProps & IWithAuthChildComponentProps & IFlashableDispatchProps) { 60 | super(props); 61 | this.state = this.state || initialState; 62 | } 63 | 64 | public componentWillMount() { 65 | this.authToken = this.props.valid_access_token; 66 | 67 | // SEE: http://infozone.se/en/authenticate-against-signalr-2/#codesyntax_5 68 | 69 | this.hubConnection = this.createhubConnection(); 70 | this.proxy = this.createProxy(this.hubConnection); 71 | 72 | //this.connectToSignalRHub(); 73 | 74 | } 75 | 76 | public componentDidUpdate() { 77 | // "This is also a good place to do network requests as 78 | // long as you compare the current props to previous props" 79 | 80 | // Object {connecting: 0, connected: 1, reconnecting: 2, disconnected: 4} 81 | //const signalRConnectionStatus = this.hubConnection.state; 82 | const isAuthUpdated = this.authToken !== this.props.valid_access_token; 83 | 84 | this.authToken = this.props.valid_access_token; 85 | if (isAuthUpdated) { 86 | // the only way to change the auth token in signalr is to 87 | // disconnect, then reset the token and connect again. 88 | this.disconnectFromSignalRHub(); 89 | this.setAuthToken(this.hubConnection, this.props.valid_access_token); 90 | this.connectToSignalRHub(); 91 | } 92 | } 93 | 94 | public invoker = (method: string, ...args: any[]): void => { 95 | if (this.authIsMissing()) { 96 | console.warn(`NOT invoking ${method}: auth is missing`); 97 | return; 98 | } 99 | this.proxy.invoke(method, ...args).done(() => { 100 | //console.log(`Invocation of ${method} succeeded`); 101 | }).fail((error) => { 102 | console.error(`Invocation of ${method} failed`, error); 103 | this.props.flashActions.addFlashErrorMessage(`Invocation of ${method} failed: ${error}`); 104 | }); 105 | } 106 | 107 | public render(): JSX.Element { 108 | 109 | const childProps = { 110 | isConnected: this.state.isConnected, 111 | invoker: this.invoker.bind(this), 112 | openConnection: this.connectToSignalRHub.bind(this), // test only 113 | closeConnection: this.disconnectFromSignalRHub.bind(this), // test only 114 | }; 115 | 116 | return ( 117 | { this.wrappedComponentRef = c; }} 118 | {...this.props} 119 | {...childProps} /> 120 | ); 121 | } 122 | 123 | 124 | 125 | // catch whatever (registered) calls come back 126 | // from the server. 127 | public serverMessageHandler(method: string, ...args: any[]) { 128 | 129 | // Dynamically invoke the child component responding 130 | // method if it exists. 131 | if (this.wrappedComponentRef[method]) { 132 | this.wrappedComponentRef[method](...args); 133 | } else { 134 | console.warn("No method " + method); 135 | } 136 | } 137 | 138 | private setAuthToken(hubConnection: SignalR.Hub.Connection, token: string) { 139 | hubConnection.qs = token ? `authtoken=${this.props.valid_access_token}` : ""; 140 | } 141 | 142 | 143 | private authIsMissing = (): boolean => { 144 | return this.props.requireauthtoken && !this.props.valid_access_token; 145 | } 146 | 147 | private createhubConnection() { 148 | 149 | const hubConnection = ($ as any).hubConnection(this.props.url); 150 | 151 | if (this.props.logging) { 152 | hubConnection.logging = true; 153 | } 154 | this.setAuthToken(hubConnection, this.props.valid_access_token); 155 | 156 | hubConnection.connectionSlow(() => this.onConnectionSlow()); 157 | hubConnection.reconnected(() => this.onReconnected()); 158 | hubConnection.starting(() => this.onStarting()); 159 | hubConnection.received(() => this.onReceived()); 160 | hubConnection.reconnecting(() => this.onReconnecting()); 161 | hubConnection.disconnected(() => this.onDisconnected()); 162 | hubConnection.stateChanged((newState) => this.onStateChange(newState)); 163 | hubConnection.error((error) => this.onError(error)); 164 | 165 | return hubConnection; 166 | } 167 | 168 | private disconnectFromSignalRHub() { 169 | 170 | if (this.authIsMissing()) { 171 | console.warn("NOT disconnecting: auth is missing"); 172 | return; 173 | } 174 | if (this.hubConnection.state !== ($ as any).signalR.connectionState.disconnected) { 175 | this.hubConnection.stop(); 176 | } 177 | } 178 | 179 | private connectToSignalRHub() { 180 | // as far as I can see, signalr doesn't fire 181 | // state change events during "start". 182 | if (this.authIsMissing()) { 183 | console.warn("NOT Connecting: auth is missing"); 184 | return; 185 | } 186 | this.hubConnection.start() 187 | .done(() => { 188 | this.onConnected(this.hubConnection.id); 189 | //console.log("Now connected, connection ID=" + this.hubConnection.id); 190 | }) 191 | .fail((error) => { 192 | console.error("Could not connect to the server."); // Where is the XHR error? 193 | this.props.flashActions.addFlashErrorMessage("Could not connect to the server."); 194 | //const errorMsg = "Could not connect to the server."; 195 | //const newState = Object.assign({}, this.state, {connectionError: errorMsg}); 196 | //this.setState(newState); 197 | }); 198 | } 199 | 200 | private createProxy(hubConnection: SignalR.Hub.Connection) { 201 | const proxy = hubConnection.createHubProxy(this.props.hub); 202 | 203 | // tslint:disable-next-line 204 | // see: https://docs.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client 205 | for (const method of this.props.clientmethods || []) { 206 | const fn = (...args: any[]) => this.serverMessageHandler(method, ...args); 207 | proxy.on(method, fn.bind(this)); 208 | } 209 | return proxy; 210 | } 211 | 212 | private getLastError(hubConnection: SignalR.Hub.Connection): Error { 213 | return hubConnection.lastError; 214 | } 215 | 216 | private onDisconnected = (): void => { 217 | console.log("onDisconnected"); 218 | 219 | this.setState({isConnected: false}); 220 | } 221 | 222 | private onReconnecting = (): void => { 223 | console.log("onReconnecting"); 224 | //this.setState(Object.assign({}, this.state, {isConnected: false})); 225 | } 226 | 227 | // "Connecting, Connected, Reconnecting, or Disconnected" ?? 228 | private onStateChange = (newState: string): void => { 229 | console.log("onStateChange", newState); 230 | } 231 | 232 | private onConnectionSlow = (): void => { 233 | console.log("onConnectionSlow"); 234 | } 235 | 236 | private onReceived = (): void => { 237 | //console.log("ON RECEIVED ====================================================="); 238 | console.log("onReceived"); 239 | } 240 | 241 | private onStarting = (): void => { 242 | console.log("onStarting"); 243 | } 244 | 245 | private onReconnected = (): void => { 246 | // TODO: set the connectionid 247 | console.log("onReconnected"); 248 | this.setState({isConnected: true}); 249 | } 250 | 251 | private onConnected = (id: string): void => { 252 | this.setState({isConnected: true}); 253 | console.log(`onConnected ${id}`); 254 | } 255 | 256 | private onError = (error: String): void => { 257 | console.error("ERROR", error); 258 | } 259 | 260 | } 261 | return withFlash(SignalRConnector); 262 | }; 263 | 264 | export default withSignalR; 265 | -------------------------------------------------------------------------------- /src/Web/src/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import flash from "./flash/flashReducers"; 4 | import oidc from "./redux-oidc/oidcReducers"; 5 | 6 | export default combineReducers({ 7 | flash, 8 | oidc, 9 | }); 10 | -------------------------------------------------------------------------------- /src/Web/src/redux-oidc/oidcActions.ts: -------------------------------------------------------------------------------- 1 | import * as OidcConstants from "./oidcConstants"; 2 | 3 | // TODO: Figure out what type this is 4 | 5 | // application events 6 | const loggedIn = (user: any) => ({ 7 | type: OidcConstants.LOGIN_LOGGED_IN, 8 | user 9 | }); 10 | 11 | const loggedOut = () => ({ 12 | type: OidcConstants.LOGIN_LOGGED_OUT 13 | }); 14 | 15 | const loginRequest = (currentRoute: string) => ({ 16 | type: OidcConstants.LOGIN_LOGIN_REQUESTED, 17 | currentRoute 18 | }); 19 | 20 | const logoutRequest = (currentRoute: string) => ({ 21 | type: OidcConstants.LOGIN_LOGOUT_REQUESTED 22 | }); 23 | 24 | 25 | const newLoginCallbackRequest = (redirectRoute: string) => ({ 26 | type: OidcConstants.LOGIN_CALLBACK_REQUESTED, 27 | redirectRoute 28 | }); 29 | 30 | 31 | const loginNetworkFailed = (message: string) => ({ 32 | type: OidcConstants.LOGIN_NETWORK_FAILED, 33 | message 34 | }); 35 | 36 | 37 | // oidc-js events 38 | 39 | const userLoaded = (user) => ({ 40 | type: OidcConstants.OIDC_USER_LOADED, 41 | user: user 42 | }); 43 | 44 | const userUnloaded = () => ({ 45 | type: OidcConstants.OIDC_USER_UNLOADED 46 | }); 47 | 48 | const userSignedOut = () => ({ 49 | type: OidcConstants.OIDC_USER_SIGNED_OUT 50 | }); 51 | 52 | const silentRenewError = () => ({ 53 | type: OidcConstants.OIDC_SILENT_RENEW_ERROR 54 | }); 55 | 56 | const accessTokenExpired = () => ({ 57 | type: OidcConstants.OIDC_ACCESS_TOKEN_EXPIRED 58 | }); 59 | 60 | const accessTokenExpiring = () => ({ 61 | type: OidcConstants.OIDC_ACCESS_TOKEN_EXPIRING 62 | }); 63 | 64 | 65 | export const oidcActionCreators: any = { 66 | loginRequest, 67 | newLoginCallbackRequest, 68 | logoutRequest, 69 | loggedIn, 70 | loggedOut, 71 | loginNetworkFailed, 72 | userLoaded, 73 | userUnloaded, 74 | userSignedOut, 75 | silentRenewError, 76 | accessTokenExpired, 77 | accessTokenExpiring, 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /src/Web/src/redux-oidc/oidcComponentWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as Redux from "redux"; 3 | import * as ReactRedux from "react-redux"; 4 | import { oidcActionCreators } from "./oidcActions"; 5 | 6 | export interface IOidcDispatchProps { 7 | // TODO: Get rid of any 8 | oidcActions: any; 9 | } 10 | 11 | export const withOidcActions = (Component: typeof React.Component) => { 12 | 13 | class OidcWrapper extends React.Component { 14 | 15 | public render(): JSX.Element { 16 | return ; 17 | } 18 | } 19 | 20 | const mapDispatchToProps: any = (dispatch): IOidcDispatchProps => ({ 21 | oidcActions: Redux.bindActionCreators(oidcActionCreators, dispatch) 22 | }); 23 | 24 | return ReactRedux.connect(null, mapDispatchToProps)(OidcWrapper) as React.ComponentClass; 25 | }; 26 | -------------------------------------------------------------------------------- /src/Web/src/redux-oidc/oidcConstants.ts: -------------------------------------------------------------------------------- 1 | export const LOGIN_LOGIN_REQUESTED = "login/LOGIN_REQUESTED"; 2 | 3 | export const LOGIN_LOGOUT_REQUESTED = "login/LOGOUT_REQUESTED"; 4 | 5 | export const LOGIN_LOGGED_IN = "login/LOGIN_LOGGED_IN"; 6 | 7 | export const LOGIN_LOGGED_OUT = "login/LOGGED_OUT"; 8 | 9 | export const LOGIN_NETWORK_FAILED = "login/LOGIN_NETWORK_FAILED"; 10 | 11 | export const LOGIN_CALLBACK_REQUESTED = "login/LOGIN_CALLBACK_REQUESTED"; 12 | 13 | // The following are OIDC-JS related events which are handled 14 | // internally. 15 | // SEE: https://github.com/IdentityModel/oidc-client-js/wiki#events 16 | 17 | // Raised when a user session has been established (or re-established). 18 | export const OIDC_USER_LOADED = "oidc/USER_LOADED"; 19 | 20 | // Raised when a user session has been terminated. 21 | export const OIDC_USER_UNLOADED = "oidc/USER_UNLOADED"; 22 | 23 | // Raised prior to the access token expiring. 24 | export const OIDC_ACCESS_TOKEN_EXPIRING = "oidc/ACCESS_TOKEN_EXPIRING"; 25 | 26 | // Raised after the access token has expired. 27 | export const OIDC_ACCESS_TOKEN_EXPIRED = "oidc/ACCESS_TOKEN_EXPIRED"; 28 | 29 | // Raised when the automatic silent renew has failed. 30 | export const OIDC_SILENT_RENEW_ERROR = "oidc/SILENT_RENEW_ERROR"; 31 | 32 | // Raised when the user's signin status at the OP has changed. 33 | // SEE: https://brockallen.com/2016/08/12/check-session-support-in-oidc-client-js/ 34 | export const OIDC_USER_SIGNED_OUT = "oidc/USER_SIGNED_OUT"; 35 | 36 | export interface IAuthState { 37 | is_authenticated: boolean; 38 | access_token?: String; 39 | expired?: boolean; 40 | expires_at?: number; 41 | id_token?: String; 42 | full_name?: String; 43 | given_name?: String; 44 | family_name?: String; 45 | username?: String; 46 | email?: String; 47 | token_type?: String; 48 | id?: String; 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/Web/src/redux-oidc/oidcMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as Oidc from "oidc-client"; 2 | import * as Redux from "redux"; 3 | import { 4 | LOGIN_LOGIN_REQUESTED, LOGIN_CALLBACK_REQUESTED, 5 | OIDC_USER_LOADED, LOGIN_LOGOUT_REQUESTED 6 | } from "./oidcConstants"; 7 | import { oidcActionCreators } from "./oidcActions"; 8 | import * as ReactRouter from "react-router"; 9 | import {createStateFromUser} from "./oidcReducers"; 10 | 11 | let _oidcManager: Oidc.UserManager; 12 | let _dispatcher: Redux.Dispatch; 13 | 14 | const loadUser = (userManager: Oidc.UserManager, dispatch: Redux.Dispatch) => { 15 | _oidcManager.getUser().then(user => { 16 | dispatch(oidcActionCreators.userLoaded(user)); 17 | 18 | }).catch((e) => { 19 | console.error(e); 20 | }); 21 | }; 22 | 23 | const configureManager = (userManager: Oidc.UserManager, dispatcher: Redux.Dispatch) => { 24 | 25 | //userManager.events.addUserLoaded((user) => { 26 | // MB: on what conditions does this actually fire? getUser doesn't seem to trigger it. 27 | //_dispatcher(oidcActionCreators.userLoaded(user)); 28 | //}); 29 | userManager.events.addUserUnloaded(() => dispatcher(oidcActionCreators.userUnloaded())); 30 | userManager.events.addUserSignedOut(() => dispatcher(oidcActionCreators.userSignedOut())); 31 | 32 | userManager.events.addSilentRenewError(() => dispatcher(oidcActionCreators.silentRenewError())); 33 | userManager.events.addAccessTokenExpired(() => dispatcher(oidcActionCreators.accessTokenExpired())); 34 | userManager.events.addAccessTokenExpiring(() => dispatcher(oidcActionCreators.accessTokenExpiring())); 35 | 36 | // TODO: remove events 37 | // events.removeUserLoaded(actions.userLoaded); 38 | // events.removeSilentRenewError(actions.silentRenewError); 39 | // events.removeAccessTokenExpired(actions.accessTokenExpired); 40 | // events.removeAccessTokenExpiring(actions.accessTokenExpiring); 41 | // events.removeUserUnloaded(actions.userUnloaded); 42 | // events.removeUserSignedOut(actions.userSignedOut); 43 | }; 44 | 45 | const signIn = (userManager: Oidc.UserManager, 46 | currentRoute: string, 47 | dispatch: Redux.Dispatch 48 | ) => { 49 | userManager.signinRedirect({ data: currentRoute }).then(() => { 50 | //console.log("redirecting..."); 51 | }).catch(reason => { 52 | if (reason.message === "Network Error") { 53 | console.error("The authentication server is currently unavailable."); 54 | dispatch(oidcActionCreators.loginNetworkFailed("The authentication server is currently unavailable.")); 55 | } else { 56 | console.error(reason.message); 57 | dispatch(oidcActionCreators.loginNetworkFailed(reason.message)); 58 | } 59 | }); 60 | }; 61 | 62 | // side effect: ReactRouter will navigate to the value of result.state 63 | const signInRedirectCallback = (userManager: Oidc.UserManager, 64 | dispatch: Redux.Dispatch) => { 65 | 66 | userManager.signinRedirectCallback().then(result => { 67 | userManager.getUser().then(user => { 68 | dispatch(oidcActionCreators.userLoaded(user)); 69 | }).catch((e) => { 70 | console.error(e); 71 | }); 72 | ReactRouter.browserHistory.push(result.state || "/"); 73 | 74 | }).catch((reason) => { 75 | console.error(reason); 76 | }); 77 | }; 78 | 79 | const signOut = (userManager: Oidc.UserManager, 80 | dispatch: Redux.Dispatch) => { 81 | 82 | userManager.signoutRedirect().then(() => { 83 | //console.log("Logged out."); 84 | }).catch(reason => { 85 | if (reason.message === "Network Error") { 86 | dispatch(oidcActionCreators.loginNetworkFailed("The authentication server is currently unavailable.")); 87 | } else { 88 | dispatch(oidcActionCreators.loginNetworkFailed(reason.message)); 89 | } 90 | }); 91 | }; 92 | 93 | // create the logged-in status for the store for when the page is reloaded 94 | export const oidcGetInitialLoginState = () => { 95 | return _oidcManager.getUser().then(user => { 96 | return { 97 | oidc: createStateFromUser(user) 98 | }; 99 | }).catch(e => console.error("ERROR", e)); 100 | }; 101 | 102 | // side-effects: we need to set the _oidcManager 103 | // and _dispatcher so that we can later create the initial state via 104 | // oidcSetInitialLoginState. 105 | export default function createOidcMiddleware(settings: Oidc.UserManagerSettings) { 106 | 107 | _oidcManager = new Oidc.UserManager(settings); 108 | 109 | const oidcAuthStateHandler = (store) => { 110 | 111 | if (!store.dispatch) { 112 | throw new Error("Dispatch is missing"); 113 | } 114 | configureManager(_oidcManager, store.dispatch); 115 | _dispatcher = store.dispatch; 116 | 117 | return next => action => { 118 | 119 | switch (action.type) { 120 | case LOGIN_LOGIN_REQUESTED: 121 | signIn(_oidcManager, action.currentRoute, store.dispatch); 122 | break; 123 | case LOGIN_CALLBACK_REQUESTED: 124 | signInRedirectCallback(_oidcManager, store.dispatch); 125 | break; 126 | case LOGIN_LOGOUT_REQUESTED: 127 | signOut(_oidcManager, store.dispatch); 128 | break; 129 | case OIDC_USER_LOADED: 130 | store.dispatch(oidcActionCreators.loggedIn(action.user)); 131 | break; 132 | default: 133 | break; 134 | } 135 | 136 | return next(action); 137 | }; 138 | }; 139 | 140 | return oidcAuthStateHandler; 141 | 142 | }; 143 | 144 | -------------------------------------------------------------------------------- /src/Web/src/redux-oidc/oidcReducers.ts: -------------------------------------------------------------------------------- 1 | import * as Redux from "redux"; 2 | import * as constants from "./oidcConstants"; 3 | import * as Oidc from "oidc-client"; 4 | 5 | const loggedOutState = { 6 | is_authenticated: false 7 | }; 8 | 9 | export const createStateFromUser = (user: Oidc.User): constants.IAuthState => { 10 | return user ? 11 | { 12 | access_token: user.access_token, 13 | expired: user.expired, 14 | expires_at: user.expires_at, 15 | id_token: user.id_token, 16 | is_authenticated: true, 17 | full_name: user.profile.name, 18 | given_name: user.profile.given_name, 19 | family_name: user.profile.family_name, 20 | username: user.profile.preferred_username, 21 | email: user.profile.email, 22 | token_type: user.token_type, 23 | id: user.profile.sub 24 | } : loggedOutState; 25 | }; 26 | 27 | export default function oidc(state: constants.IAuthState = loggedOutState, action: any) { 28 | 29 | switch (action.type) { 30 | case constants.OIDC_USER_UNLOADED: 31 | //console.log("Reducers: User Unloaded"); 32 | return Object.assign({}, state, loggedOutState); 33 | 34 | case constants.OIDC_USER_LOADED: 35 | //console.log("Reducers: User Loaded"); 36 | return Object.assign({}, state, createStateFromUser(action.user)); 37 | 38 | case constants.OIDC_USER_SIGNED_OUT: 39 | //console.log("Reducers: User SIGNED OUT"); 40 | return Object.assign({}, state, loggedOutState); 41 | 42 | case constants.OIDC_ACCESS_TOKEN_EXPIRED: 43 | // console.log("ACCESS TOKEN EXPIRED"); 44 | return state; 45 | 46 | case constants.OIDC_ACCESS_TOKEN_EXPIRING: 47 | // console.log("ACCESS TOKEN EXPIRING"); 48 | return state; 49 | 50 | case constants.OIDC_SILENT_RENEW_ERROR: 51 | // console.log("SILENT RENEW ERROR"); 52 | return Object.assign({}, state, loggedOutState); 53 | 54 | case constants.LOGIN_CALLBACK_REQUESTED: 55 | // console.log("SILENT RENEW ERROR"); 56 | return state; 57 | 58 | default: 59 | return state; 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/Web/src/redux-oidc/withAuthToken.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactRedux from "react-redux"; 3 | 4 | // this is what child components will receive in 5 | // props. 6 | export interface IWithAuthChildComponentProps { 7 | requireauthtoken: boolean; 8 | valid_access_token?: string; 9 | valid_identity_token?: string; 10 | } 11 | 12 | interface IWithAuthTokenState {} 13 | 14 | // TODO: these should just be passed through, 15 | // not declared 16 | interface IWithAuthTokenOwnProps { 17 | hub: string; 18 | url: string; 19 | logging?: boolean; 20 | clientmethods?: string[]; 21 | } 22 | 23 | interface IAuthStatus { 24 | is_authenticated: boolean; 25 | id?: string; 26 | valid_access_token?: string; 27 | valid_identity_token?: string; 28 | } 29 | 30 | export interface IWithAuthTokenDispatchProps { 31 | auth: IAuthStatus; 32 | } 33 | 34 | // 35 | // Respond to changes in authentication / identity status 36 | // and pass that information to the child component. 37 | // 38 | //export const withAuthToken = (Component: typeof React.Component) => { 39 | export const withAuthToken = (Component: React.ComponentClass) => { 40 | 41 | class WithAuthToken extends React.Component { 43 | 44 | //public static propTypes: React.ValidationMap = { 45 | // var1: React.PropTypes.number.isRequired, 46 | // actions: React.PropTypes.shape({ 47 | // myfunc: React.PropTypes.func.isRequired 48 | // }), 49 | // flashActions: React.PropTypes.shape({ 50 | // addFlashSuccessMessage: React.PropTypes.func.isRequired, 51 | // addFlashErrorMessage: React.PropTypes.func.isRequired, 52 | // }), 53 | //}; 54 | 55 | public render(): JSX.Element { 56 | 57 | const childProps: IWithAuthChildComponentProps = { 58 | requireauthtoken: true, 59 | valid_access_token: this.props.auth.valid_access_token, 60 | valid_identity_token: this.props.auth.valid_identity_token 61 | }; 62 | 63 | const {auth, ...filteredprops} = this.props; 64 | 65 | return ( 66 | 67 | ); 68 | } 69 | } 70 | 71 | const mapStateToProps: any = (state): IWithAuthTokenDispatchProps => ({ 72 | auth: { 73 | id: state.oidc.id, 74 | valid_access_token: state.oidc.access_token, 75 | valid_identity_token: state.oidc.id_token, 76 | is_authenticated: state.oidc.is_authenticated 77 | } 78 | }); 79 | 80 | return ReactRedux.connect(mapStateToProps)(WithAuthToken) as React.ComponentClass; 81 | 82 | }; 83 | 84 | 85 | export default withAuthToken; 86 | -------------------------------------------------------------------------------- /src/Web/src/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import reducers from "./reducers"; 3 | import createOidcMiddleware from "./redux-oidc/oidcMiddleware"; 4 | import * as ReduxObservable from "redux-observable"; 5 | import {oidcImplicitSettings} from "./config/oidcConfig"; 6 | import epics from "./epics"; 7 | 8 | const epicMiddleware = ReduxObservable.createEpicMiddleware(epics); 9 | const oidcMiddleware = createOidcMiddleware(oidcImplicitSettings); 10 | 11 | //const devToolsExtension = (window as any).devToolsExtension && (window as any).devToolsExtension(); 12 | const devToolsExtension = (window as any).devToolsExtension ? (window as any).devToolsExtension() : f => f; 13 | 14 | const createStoreWithInitialState = (initialState) => createStore( 15 | reducers, 16 | initialState, 17 | compose( 18 | applyMiddleware(oidcMiddleware, epicMiddleware), 19 | devToolsExtension 20 | ) 21 | ); 22 | 23 | export default createStoreWithInitialState; 24 | 25 | -------------------------------------------------------------------------------- /src/Web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": false, 16 | "strictNullChecks": false, 17 | "suppressImplicitAnyIndexErrors": true, 18 | //"noUnusedLocals": true, 19 | "noUnusedLocals": false // TODO: Turn this back on 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "build", 24 | "scripts", 25 | "acceptance-tests", 26 | "webpack", 27 | "jest", 28 | "src/setupTests.ts" 29 | ], 30 | "types": [ 31 | "typePatches" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/Web/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react"], 3 | "rules": { 4 | "align": [ 5 | true, 6 | "parameters", 7 | "arguments", 8 | "statements" 9 | ], 10 | "ban": false, 11 | "class-name": true, 12 | "comment-format": [ 13 | false, 14 | "check-space" 15 | ], 16 | "curly": true, 17 | "eofline": false, 18 | "forin": true, 19 | "indent": [ true, "spaces" ], 20 | "interface-name": true, 21 | "jsdoc-format": true, 22 | "jsx-no-lambda": false, 23 | "jsx-no-multiline-js": false, 24 | "jsx-alignment": false, 25 | "label-position": true, 26 | "max-line-length": [ true, 120 ], 27 | "member-ordering": [ 28 | true, 29 | "public-before-private", 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-any": false, 34 | "no-arg": true, 35 | "no-bitwise": true, 36 | "no-console": [ 37 | false, 38 | "log", 39 | "error", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-construct": true, 48 | "no-debugger": true, 49 | "no-duplicate-variable": true, 50 | "no-empty": true, 51 | "no-eval": true, 52 | "no-shadowed-variable": true, 53 | "no-string-literal": true, 54 | "no-switch-case-fall-through": true, 55 | "no-trailing-whitespace": false, 56 | "no-unused-expression": true, 57 | "no-use-before-declare": true, 58 | "one-line": [ 59 | true, 60 | "check-catch", 61 | "check-else", 62 | "check-open-brace", 63 | "check-whitespace" 64 | ], 65 | "quotemark": [true, "double", "jsx-double"], 66 | "radix": true, 67 | "semicolon": [true, "always"], 68 | "switch-default": true, 69 | 70 | "trailing-comma": false, 71 | 72 | "triple-equals": [ true, "allow-null-check" ], 73 | "typedef": [ 74 | true, 75 | "parameter", 76 | "property-declaration" 77 | ], 78 | "typedef-whitespace": [ 79 | true, 80 | { 81 | "call-signature": "nospace", 82 | "index-signature": "nospace", 83 | "parameter": "nospace", 84 | "property-declaration": "nospace", 85 | "variable-declaration": "nospace" 86 | } 87 | ], 88 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 89 | "whitespace": [ 90 | true, 91 | "check-branch", 92 | "check-decl", 93 | "check-module", 94 | "check-operator", 95 | "check-separator", 96 | "check-type", 97 | "check-typecast" 98 | ] 99 | } 100 | } 101 | --------------------------------------------------------------------------------