Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,3 +333,6 @@ ASALocalRun/

# MacOS
.DS_Store

# Fake database
*fakedatabase.db
Original file line numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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