Skip to content
This repository has been archived by the owner on Sep 18, 2021. It is now read-only.

Add public clients support #3653

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions source/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ public static class AuthenticationMethods

public static class ParsedSecretTypes
{
public const string NoSecret = "NoSecret";
public const string SharedSecret = "SharedSecret";
public const string X509Certificate = "X509Certificate";
public const string JwtBearer = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
Expand Down
6 changes: 6 additions & 0 deletions source/Core/Models/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class Client
/// </summary>
public List<Secret> ClientSecrets { get; set; }

/// <summary>
/// If set to false, no client secret is needed to request tokens at the token endpoint (defaults to true)
/// </summary>
public bool RequireClientSecret { get; set; }

/// <summary>
/// Client display name (used for logging and consent screen)
/// </summary>
Expand Down Expand Up @@ -254,6 +259,7 @@ public Client()
Flow = Flows.Implicit;

ClientSecrets = new List<Secret>();
RequireClientSecret = true;
AllowedScopes = new List<string>();
RedirectUris = new List<string>();
PostLogoutRedirectUris = new List<string>();
Expand Down
36 changes: 21 additions & 15 deletions source/Core/Validation/ClientSecretValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,32 @@ public async Task<ClientSecretValidationResult> ValidateAsync()
return fail;
}

var result = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets);

if (result.Success)
if (!client.RequireClientSecret)
{
Logger.Info("Client validation success");

var success = new ClientSecretValidationResult
Logger.Info("Public Client - skipping secret validation success");
}
else
{
var result = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets);
if (!result.Success)
{
IsError = false,
Client = client
};
await RaiseFailureEvent(client.ClientId, "Invalid client secret");
Logger.Info("Client validation failed.");

await RaiseSuccessEvent(client.ClientId);
return success;
return fail;
}
}

await RaiseFailureEvent(client.ClientId, "Invalid client secret");
Logger.Info("Client validation failed.");

return fail;
Logger.Info("Client validation success");

var success = new ClientSecretValidationResult
{
IsError = false,
Client = client
};

await RaiseSuccessEvent(client.ClientId);
return success;
}

private async Task RaiseSuccessEvent(string clientId)
Expand Down
40 changes: 31 additions & 9 deletions source/Core/Validation/PostBodySecretParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,43 @@ public async Task<ParsedSecret> ParseAsync(IDictionary<string, object> environme
var id = body.Get("client_id");
var secret = body.Get("client_secret");

if (id.IsPresent() && secret.IsPresent())
// client id must be present
if (id.IsPresent())
{
if (id.Length > _options.InputLengthRestrictions.ClientId ||
secret.Length > _options.InputLengthRestrictions.ClientSecret)
if (id.Length > _options.InputLengthRestrictions.ClientId)
{
Logger.Debug("Client ID or secret exceeds maximum lenght.");
Logger.Debug("Client ID exceeds maximum length.");
return null;
}

var parsedSecret = new ParsedSecret
ParsedSecret parsedSecret;

if (secret.IsPresent())
{
if (secret.Length > _options.InputLengthRestrictions.ClientSecret)
{
Logger.Debug("Client secret exceeds maximum length.");
return null;
}

parsedSecret = new ParsedSecret
{
Id = id,
Credential = secret,
Type = Constants.ParsedSecretTypes.SharedSecret
};
}
else
{
Id = id,
Credential = secret,
Type = Constants.ParsedSecretTypes.SharedSecret
};
// client secret is optional
Logger.Debug("Client id without secret found");

parsedSecret = new ParsedSecret
{
Id = id,
Type = Constants.ParsedSecretTypes.NoSecret
};
}

return parsedSecret;
}
Expand Down
18 changes: 14 additions & 4 deletions source/Core/Validation/SecretParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,29 @@ public SecretParser(IEnumerable<ISecretParser> parsers)
public async Task<ParsedSecret> ParseAsync(IDictionary<string, object> environment)
{
// see if a registered parser finds a secret on the request
ParsedSecret parsedSecret = null;
ParsedSecret bestSecret = null;
foreach (var parser in _parsers)
{
parsedSecret = await parser.ParseAsync(environment);
var parsedSecret = await parser.ParseAsync(environment);
if (parsedSecret != null)
{
Logger.DebugFormat("Parser found secret: {0}", parser.GetType().Name);
Logger.InfoFormat("Secret id found: {0}", parsedSecret.Id);

return parsedSecret;
bestSecret = parsedSecret;

if (parsedSecret.Type != Constants.ParsedSecretTypes.NoSecret)
{
break;
}
}
}

if (bestSecret != null)
{
Logger.InfoFormat("Secret id found: {0}", bestSecret.Id);
return bestSecret;
}

Logger.InfoFormat("Parser found no secret");
return null;
}
Expand Down
1 change: 1 addition & 0 deletions source/Tests/UnitTests/Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
<Compile Include="TokenClients\ClientCredentialsClient.cs" />
<Compile Include="TokenClients\CustomGrantClient.cs" />
<Compile Include="TokenClients\RefreshTokenClient.cs" />
<Compile Include="TokenClients\ResourceOwnerPublicClient.cs" />
<Compile Include="TokenClients\ResourceOwnerClient.cs" />
<Compile Include="TokenClients\Setup\Clients.cs" />
<Compile Include="TokenClients\Setup\CustomGrantValidator.cs" />
Expand Down
14 changes: 14 additions & 0 deletions source/Tests/UnitTests/TokenClients/ResourceOwnerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,20 @@ public async Task Invalid_Password()
response.Error.Should().Be("invalid_grant");
}

[Fact]
public async Task Missing_Client_Secret()
{
var client = new TokenClient(
TokenEndpoint,
"roclient",
innerHttpMessageHandler: _handler);

var response = await client.RequestResourceOwnerPasswordAsync("bob", "bob", "api1");

response.IsError.Should().Be(true);
response.Error.Should().Be("invalid_client");
}


private Dictionary<string, object> GetPayload(TokenResponse response)
{
Expand Down
98 changes: 98 additions & 0 deletions source/Tests/UnitTests/TokenClients/ResourceOwnerPublicClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using FluentAssertions;
using IdentityModel;
using IdentityModel.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.Owin.Builder;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace IdentityServer3.Tests.TokenClients
{
public class ResourceOwnerPublicClient
{
const string TokenEndpoint = "https://server/connect/token";

private readonly HttpClient _client;
private readonly OwinHttpMessageHandler _handler;

public ResourceOwnerPublicClient()
{
var app = TokenClientIdentityServer.Create();
_handler = new OwinHttpMessageHandler(app.Build());
_client = new HttpClient(_handler);
}

[Fact]
public async Task Valid_User()
{
var client = new TokenClient(
TokenEndpoint,
"roclient.public",
innerHttpMessageHandler: _handler);

var response = await client.RequestResourceOwnerPasswordAsync("bob", "bob", "api1");

response.IsError.Should().Be(false);
response.ExpiresIn.Should().Be(3600);
response.TokenType.Should().Be("Bearer");
response.IdentityToken.Should().BeNull();
response.RefreshToken.Should().BeNull();

var payload = GetPayload(response);

payload.Count().Should().Be(10);
payload.Should().Contain("iss", "https://idsrv3");
payload.Should().Contain("aud", "https://idsrv3/resources");
payload.Should().Contain("client_id", "roclient.public");
payload.Should().Contain("scope", "api1");
payload.Should().Contain("sub", "88421113");
payload.Should().Contain("idp", "idsrv");

var amr = payload["amr"] as JArray;
amr.Count().Should().Be(1);
amr.First().ToString().Should().Be("password");
}

[Fact]
public async Task Unknown_User()
{
var client = new TokenClient(
TokenEndpoint,
"roclient.public",
innerHttpMessageHandler: _handler);

var response = await client.RequestResourceOwnerPasswordAsync("unknown", "bob", "api1");

response.IsError.Should().Be(true);
response.Error.Should().Be("invalid_grant");
}

[Fact]
public async Task Invalid_Password()
{
var client = new TokenClient(
TokenEndpoint,
"roclient.public",
innerHttpMessageHandler: _handler);

var response = await client.RequestResourceOwnerPasswordAsync("bob", "invalid", "api1");

response.IsError.Should().Be(true);
response.Error.Should().Be("invalid_grant");
}

private Dictionary<string, object> GetPayload(TokenResponse response)
{
var token = response.AccessToken.Split('.').Skip(1).Take(1).First();
var dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(
Encoding.UTF8.GetString(Base64Url.Decode(token)));

return dictionary;
}
}
}
16 changes: 16 additions & 0 deletions source/Tests/UnitTests/TokenClients/Setup/Clients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ public static IEnumerable<Client> Get()
}
},

//////////////////////////////////////////////////////////
// Console Resource Owner Flow with Public Client Sample
//////////////////////////////////////////////////////////
new Client
{
ClientId = "roclient.public",
RequireClientSecret = false,

Flow = Flows.ResourceOwner,

AllowedScopes = new List<string>
{
"api1", "api2"
}
},

///////////////////////////////////////////
// Console Custom Grant Flow Sample
//////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ public async void Missing_ClientSecret()

var secret = await _parser.ParseAsync(context.Environment);

secret.Should().BeNull();
secret.Should().NotBeNull();
secret.Type.Should().Be(Constants.ParsedSecretTypes.NoSecret);
secret.Id.Should().Be("client");
secret.Credential.Should().BeNull();
}

[Fact]
Expand Down