Skip to content
Merged
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,6 @@ ASALocalRun/

# MacOS
.DS_Store

# Fake database
*fakedatabase.db
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Test;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -82,36 +81,33 @@ public async Task<IActionResult> Login(LoginInputModel model, string button)
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
if (context == null)
return Redirect("~/");

// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);

return Redirect(model.ReturnUrl);
}
else
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}

return Redirect(model.ReturnUrl);

// since we don't have a valid context, then we just go back to the home page
}

if (ModelState.IsValid)
{
// validate username/password against in-memory store
if (_users.ValidateCredentials(model.Username, model.Password))
if (await _users.ValidateCredentials(model.Username, model.Password))
{
var user = _users.FindByUsername(model.Username);
var user = await _users.FindByUsername(model.Username);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.ClientId));

// only set explicit expiration here if user chooses "remember me".
Expand All @@ -124,7 +120,7 @@ public async Task<IActionResult> Login(LoginInputModel model, string button)
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
}

// issue authentication cookie with subject ID and username
await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
Expand All @@ -144,18 +140,13 @@ public async Task<IActionResult> Login(LoginInputModel model, string button)

// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{

if (string.IsNullOrEmpty(model.ReturnUrl))
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}

// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}

await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.ClientId));
Expand All @@ -166,7 +157,6 @@ public async Task<IActionResult> Login(LoginInputModel model, string button)
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}


/// <summary>
/// Show logout page
Expand Down Expand Up @@ -218,7 +208,7 @@ public async Task<IActionResult> Logout(LogoutInputModel model)
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}

return View("LoggedOut", vm);
return Redirect("/");
}

[HttpGet]
Expand All @@ -227,7 +217,6 @@ public IActionResult AccessDenied()
return View();
}


/*****************************************/
/* helper APIs for the AccountController */
/*****************************************/
Expand All @@ -243,7 +232,7 @@ private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
{
EnableLocalLogin = local,
ReturnUrl = returnUrl,
Username = context?.LoginHint
Username = context.LoginHint
};

if (!local)
Expand Down Expand Up @@ -332,7 +321,7 @@ private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logou
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.


using System;

namespace src
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
using IdentityModel;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using OpenActive.FakeDatabase.NET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

Expand All @@ -23,11 +16,11 @@ namespace IdentityServer
// [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Consumes("application/json")]
[Produces("application/json")]
public class ClientResistrationController : ControllerBase
public class ClientRegistrationController : ControllerBase
{
private readonly IClientStore _clients;

public ClientResistrationController(IClientStore clients)
public ClientRegistrationController(IClientStore clients)
{
_clients = clients;
}
Expand All @@ -38,59 +31,46 @@ public ClientResistrationController(IClientStore clients)
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> PostAsync([FromBody] ClientRegistrationModel model)
{
if (model.GrantTypes == null)
{
model.GrantTypes = new[] { OidcConstants.GrantTypes.AuthorizationCode, OidcConstants.GrantTypes.RefreshToken };
}
model.GrantTypes ??= new[] {OidcConstants.GrantTypes.AuthorizationCode, OidcConstants.GrantTypes.RefreshToken};

if (model.GrantTypes.Any(x => x == OidcConstants.GrantTypes.Implicit) || model.GrantTypes.Any(x => x == OidcConstants.GrantTypes.AuthorizationCode))
{
if (!model.RedirectUris.Any())
{
return BadRequest("A redirect URI is required for the supplied grant type.");
}

if (model.RedirectUris.Any(redirectUri => !Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)))
{
return BadRequest("One or more of the redirect URIs are invalid.");
}
}

// generate a secret for the client
var key = KeyGenerator.GenerateSecret();

StringValues headerValues;
var registrationKey = string.Empty;

if (Request.Headers.TryGetValue("Authorization", out headerValues))
{
if (Request.Headers.TryGetValue("Authorization", out var headerValues))
registrationKey = headerValues.FirstOrDefault().Substring("Bearer ".Length);
}

// update the booking system
var bookingPartner = FakeBookingSystem.Database.GetBookingPartnerByInitialAccessToken(registrationKey);
var bookingPartner = await BookingPartnerTable.GetByInitialAccessToken(registrationKey);
if (bookingPartner == null)
return Unauthorized("Initial Access Token is not valid, or is expired");

bookingPartner.Registered = true;
bookingPartner.ClientSecret = key.Sha256();
bookingPartner.Name = model.ClientName;
bookingPartner.ClientProperties = new OpenActive.FakeDatabase.NET.ClientModel
{
ClientUri = model.ClientUri,
LogoUri = model.LogoUri,
GrantTypes = model.GrantTypes,
RedirectUris = model.RedirectUris,
Scope = model.Scope,
};
FakeBookingSystem.Database.SaveBookingPartner(bookingPartner);
bookingPartner.ClientUri = model.ClientUri;
bookingPartner.RestoreAccessUri = model.ClientRegistrationUri;
bookingPartner.LogoUri = model.LogoUri;
bookingPartner.GrantTypes = model.GrantTypes;
bookingPartner.RedirectUris = model.RedirectUris;
bookingPartner.Scope = model.Scope;

await BookingPartnerTable.Save(bookingPartner);

// Read the updated client from the database and reflect back in the request
var client = await _clients.FindClientByIdAsync(bookingPartner.ClientId);
if (bookingPartner.ClientSecret != client.ClientSecrets?.FirstOrDefault()?.Value)
{
return Problem(title: "New client secret not updated in cache", statusCode: 500);
}

var response = new ClientRegistrationResponse
{
ClientId = client.ClientId,
Expand All @@ -103,7 +83,8 @@ public async Task<IActionResult> PostAsync([FromBody] ClientRegistrationModel mo
Scope = string.Join(' ', client.AllowedScopes)
};

return CreatedAtAction("ClientRegistration", response);
var baseUrl = $"{Request.Scheme}://{Request.Host.Value}/connect/register";
return Created($"{baseUrl}/{client.ClientId}", response);
}
}

Expand All @@ -115,14 +96,17 @@ public class ClientRegistrationModel
[JsonPropertyName(OidcConstants.ClientMetadata.ClientUri)]
public string ClientUri { get; set; }

[JsonPropertyName(OidcConstants.ClientMetadata.InitiateLoginUris)]
public string ClientRegistrationUri { get; set; }

[JsonPropertyName(OidcConstants.ClientMetadata.LogoUri)]
public string LogoUri { get; set; }

[JsonPropertyName(OidcConstants.ClientMetadata.GrantTypes)]
public string[] GrantTypes { get; set; }

[JsonPropertyName(OidcConstants.ClientMetadata.RedirectUris)]
public string[] RedirectUris { get; set; } = new string[] {};
public string[] RedirectUris { get; set; } = {};

public string Scope { get; set; } = "openid profile email";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.Extensions.Logging;
using OpenActive.FakeDatabase.NET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -11,34 +9,36 @@ namespace IdentityServer
{
public class ClientStore : IClientStore
{
public Task<Client> FindClientByIdAsync(string clientId)
public async Task<Client> FindClientByIdAsync(string clientId)
{
var bookingPartner = FakeBookingSystem.Database.GetBookingPartner(clientId);
return Task.FromResult(this.ConvertToIS4Client(bookingPartner));
var bookingPartner = await BookingPartnerTable.GetByClientId(clientId);
return ConvertToIs4Client(bookingPartner);
}

private Client ConvertToIS4Client(BookingPartnerTable bookingPartner)
private static Client ConvertToIs4Client(BookingPartnerTable bookingPartner)
{
if (bookingPartner == null) return null;
return new Client()
if (bookingPartner == null)
return null;

return new Client
{
Enabled = bookingPartner.Registered,
ClientId = bookingPartner.ClientId,
ClientName = bookingPartner.Name,
AllowedGrantTypes = bookingPartner.ClientProperties?.GrantTypes == null ? new List<string>() : bookingPartner.ClientProperties.GrantTypes.ToList(),
ClientSecrets = bookingPartner.ClientSecret == null ? new List<Secret>() : new List<Secret>() { new Secret(bookingPartner.ClientSecret) },
AllowedScopes = bookingPartner.ClientProperties?.Scope == null ? new List<string>() : bookingPartner.ClientProperties.Scope.Split(' ').ToList(),
AllowedGrantTypes = bookingPartner.GrantTypes == null ? new List<string>() : bookingPartner.GrantTypes.ToList(),
ClientSecrets = bookingPartner.ClientSecret == null ? new List<Secret>() : new List<Secret> { new Secret(bookingPartner.ClientSecret) },
AllowedScopes = bookingPartner.Scope == null ? new List<string>() : bookingPartner.Scope.Split(' ').ToList(),
Claims = bookingPartner.ClientId == null ? new List<System.Security.Claims.Claim>() : new List<System.Security.Claims.Claim>() { new System.Security.Claims.Claim("https://openactive.io/clientId", bookingPartner.ClientId) },
ClientClaimsPrefix = "",
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowOfflineAccess = true,
UpdateAccessTokenClaimsOnRefresh = true,
RedirectUris = bookingPartner.ClientProperties?.RedirectUris == null ? new List<string>() : bookingPartner.ClientProperties.RedirectUris.ToList(),
RedirectUris = bookingPartner.RedirectUris == null ? new List<string>() : bookingPartner.RedirectUris.ToList(),
RequireConsent = true,
RequirePkce = true,
LogoUri = bookingPartner.ClientProperties?.LogoUri,
ClientUri = bookingPartner.ClientProperties?.ClientUri
LogoUri = bookingPartner.LogoUri,
ClientUri = bookingPartner.ClientUri
};
}
}
Expand Down
Loading