Refactor common auth components to new Base library, enable HttpClien… (#9)
* Refactor common auth components to new Base library, enable HttpClient injection or more static usage, add Tests for WCF & OWIN #5 [#154972918, #158446551] * cleaner separation of client_credentials and authorization_code flows, connect more dots with new params (logging, httpclient, etc) * Use the redirectUri when exchanging code for token * Move towards only having one CloudFoundryTokenKeyResolver
This commit is contained in:
Родитель
db97b64848
Коммит
25f17f498e
32
Security.sln
32
Security.sln
|
@ -1,6 +1,6 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27130.2010
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.28315.86
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7608EFF0-BADC-4259-A760-FA312F9887CD}"
|
||||
EndProject
|
||||
|
@ -42,6 +42,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.DataProte
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.DataProtection.CredHubBase.Test", "test\Steeltoe.Security.DataProtection.CredHubBase.Test\Steeltoe.Security.DataProtection.CredHubBase.Test.csproj", "{5F0D59A7-FB7D-4CA6-8DC4-2BDF50D176AF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authentication.CloudFoundryBase", "src\Steeltoe.Security.Authentication.CloudFoundryBase\Steeltoe.Security.Authentication.CloudFoundryBase.csproj", "{F5FA179B-3731-485B-B2D3-994AE05166EA}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steeltoe.Security.Authentication.CloudFoundryBase.Test", "test\Steeltoe.Security.Authentication.CloudFoundryBaseTest\Steeltoe.Security.Authentication.CloudFoundryBase.Test.csproj", "{350CC18B-D550-4207-9023-E90C9B61BF6C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.Authentication.CloudFoundryOwin.Test", "test\Steeltoe.Security.Authentication.CloudFoundryOwin.Test\Steeltoe.Security.Authentication.CloudFoundryOwin.Test.csproj", "{859C51B8-D6DF-473B-959A-4F0F1533ADD2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Steeltoe.Security.Authentication.CloudFoundryWcf.Test", "test\Steeltoe.Security.Authentication.CloudFoundryWcf.Test\Steeltoe.Security.Authentication.CloudFoundryWcf.Test.csproj", "{950B0827-634F-43F9-86C4-4E615ED36A22}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -88,6 +96,22 @@ Global
|
|||
{5F0D59A7-FB7D-4CA6-8DC4-2BDF50D176AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F0D59A7-FB7D-4CA6-8DC4-2BDF50D176AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F0D59A7-FB7D-4CA6-8DC4-2BDF50D176AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F5FA179B-3731-485B-B2D3-994AE05166EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F5FA179B-3731-485B-B2D3-994AE05166EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F5FA179B-3731-485B-B2D3-994AE05166EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F5FA179B-3731-485B-B2D3-994AE05166EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{350CC18B-D550-4207-9023-E90C9B61BF6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{350CC18B-D550-4207-9023-E90C9B61BF6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{350CC18B-D550-4207-9023-E90C9B61BF6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{350CC18B-D550-4207-9023-E90C9B61BF6C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{859C51B8-D6DF-473B-959A-4F0F1533ADD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{859C51B8-D6DF-473B-959A-4F0F1533ADD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{859C51B8-D6DF-473B-959A-4F0F1533ADD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{859C51B8-D6DF-473B-959A-4F0F1533ADD2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{950B0827-634F-43F9-86C4-4E615ED36A22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{950B0827-634F-43F9-86C4-4E615ED36A22}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{950B0827-634F-43F9-86C4-4E615ED36A22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{950B0827-634F-43F9-86C4-4E615ED36A22}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -103,6 +127,10 @@ Global
|
|||
{B5EDB484-9639-4F81-9E8E-639EA866ECA6} = {BA3B33D3-64A5-4219-B4C2-787C4BD505F7}
|
||||
{E698B09A-4865-49B4-A132-DD91701CF1C9} = {7608EFF0-BADC-4259-A760-FA312F9887CD}
|
||||
{5F0D59A7-FB7D-4CA6-8DC4-2BDF50D176AF} = {BA3B33D3-64A5-4219-B4C2-787C4BD505F7}
|
||||
{F5FA179B-3731-485B-B2D3-994AE05166EA} = {7608EFF0-BADC-4259-A760-FA312F9887CD}
|
||||
{350CC18B-D550-4207-9023-E90C9B61BF6C} = {BA3B33D3-64A5-4219-B4C2-787C4BD505F7}
|
||||
{859C51B8-D6DF-473B-959A-4F0F1533ADD2} = {BA3B33D3-64A5-4219-B4C2-787C4BD505F7}
|
||||
{950B0827-634F-43F9-86C4-4E615ED36A22} = {BA3B33D3-64A5-4219-B4C2-787C4BD505F7}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BED45957-EDAD-4686-B959-BD5DA962F7B0}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<JwtTokensVersion>5.2.2</JwtTokensVersion>
|
||||
<TestSdkVersion>15.7.2</TestSdkVersion>
|
||||
<MockHttp>4.0.0</MockHttp>
|
||||
<MoqVersion>4.8.2</MoqVersion>
|
||||
<XunitAnalyzersVersion>0.9.0</XunitAnalyzersVersion>
|
||||
<XunitVersion>2.3.1</XunitVersion>
|
||||
<XunitStudioVersion>2.3.1</XunitStudioVersion>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<JwtTokensVersion>5.2.2</JwtTokensVersion>
|
||||
<TestSdkVersion>15.7.2</TestSdkVersion>
|
||||
<MockHttp>4.0.0</MockHttp>
|
||||
<MoqVersion>4.8.2</MoqVersion>
|
||||
<XunitAnalyzersVersion>0.9.0</XunitAnalyzersVersion>
|
||||
<XunitVersion>2.3.1</XunitVersion>
|
||||
<XunitStudioVersion>2.3.1</XunitStudioVersion>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<JwtTokensVersion>5.2.2</JwtTokensVersion>
|
||||
<TestSdkVersion>15.7.2</TestSdkVersion>
|
||||
<MockHttp>4.0.0</MockHttp>
|
||||
<MoqVersion>4.8.2</MoqVersion>
|
||||
<XunitAnalyzersVersion>0.9.0</XunitAnalyzersVersion>
|
||||
<XunitVersion>2.3.1</XunitVersion>
|
||||
<XunitStudioVersion>2.3.1</XunitStudioVersion>
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public class AuthServerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the location of the OAuth server
|
||||
/// </summary>
|
||||
public string AuthorizationUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location the user is sent to after authentication
|
||||
/// </summary>
|
||||
public string CallbackUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the application's client id for interacting with the auth server
|
||||
/// </summary>
|
||||
public string ClientId { get; set; } = CloudFoundryDefaults.ClientId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the application's client secret for interacting with the auth server
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; } = CloudFoundryDefaults.ClientSecret;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the authentication type currently in use
|
||||
/// </summary>
|
||||
public string SignInAsAuthenticationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout (in ms) for calls to the auth server
|
||||
/// </summary>
|
||||
public int ClientTimeout { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional scopes beyond 'openid' when requesting tokens
|
||||
/// </summary>
|
||||
public string AdditionalTokenScopes { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a scopes to require
|
||||
/// </summary>
|
||||
public string[] RequiredScopes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of additional audiences to use with token validation
|
||||
/// </summary>
|
||||
public string[] AdditionalAudiences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to validate SSO server certificate
|
||||
/// </summary>
|
||||
public bool ValidateCertificates { get; set; } = true;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public class CloudFoundryDefaults
|
||||
|
@ -21,14 +23,30 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
|
||||
public const string AuthenticationScheme = "CloudFoundry";
|
||||
public const string DisplayName = "CloudFoundry";
|
||||
|
||||
public const string AuthorizationUri = "/oauth/authorize";
|
||||
public const string AccessTokenUri = "/oauth/token";
|
||||
public const string UserInfoUri = "/userinfo";
|
||||
public const string CheckTokenUri = "/check_token";
|
||||
public const string JwtTokenKey = "/token_keys";
|
||||
public const string JwtTokenUri = "/token_keys";
|
||||
|
||||
[Obsolete("Use JwtTokenUri instead")]
|
||||
public const string JwtTokenKey = JwtTokenUri;
|
||||
|
||||
public const string OAuthServiceUrl = "Default_OAuthServiceUrl";
|
||||
public const string ClientId = "Default_ClientId";
|
||||
public const string ClientSecret = "Default_ClientSecret";
|
||||
public const string CallbackPath = "/signin-cloudfoundry";
|
||||
|
||||
public const bool ValidateCertificates = true;
|
||||
|
||||
public const string ParamsClientId = "client_id";
|
||||
public const string ParamsClientSecret = "client_secret";
|
||||
public const string ParamsResponseType = "response_type";
|
||||
public const string ParamsScope = "scope";
|
||||
public const string ParamsRedirectUri = "redirect_uri";
|
||||
public const string ParamsGrantType = "grant_type";
|
||||
public const string ParamsTokenFormat = "token_format";
|
||||
public const string ParamsCode = "code";
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Steeltoe.Common;
|
||||
using System;
|
||||
|
@ -24,36 +25,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
{
|
||||
private static DateTime baseTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public static DateTime GetIssueTime(JObject payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
}
|
||||
|
||||
var time = payload.Value<long>("iat");
|
||||
return ToAbsoluteUTC(time);
|
||||
}
|
||||
|
||||
public static DateTime GetExpTime(JObject payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
}
|
||||
|
||||
var time = payload.Value<long>("exp");
|
||||
return ToAbsoluteUTC(time);
|
||||
}
|
||||
|
||||
private static DateTime ToAbsoluteUTC(long secondsPastEpoch)
|
||||
{
|
||||
return baseTime.AddSeconds(secondsPastEpoch);
|
||||
}
|
||||
|
||||
#pragma warning disable SA1202 // Elements must be ordered by access
|
||||
public static List<string> GetScopes(JObject user)
|
||||
#pragma warning restore SA1202 // Elements must be ordered by access
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
|
@ -67,15 +39,13 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
return result;
|
||||
}
|
||||
|
||||
var asValue = scopes as JValue;
|
||||
if (asValue != null)
|
||||
if (scopes is JValue asValue)
|
||||
{
|
||||
result.Add(asValue.Value<string>());
|
||||
return result;
|
||||
}
|
||||
|
||||
var asArray = scopes as JArray;
|
||||
if (asArray != null)
|
||||
if (scopes is JArray asArray)
|
||||
{
|
||||
foreach (string s in asArray)
|
||||
{
|
||||
|
@ -106,5 +76,62 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static TokenValidationParameters GetTokenValidationParameters(TokenValidationParameters parameters, string keyUrl, HttpMessageHandler handler, bool validateCertificates, AuthServerOptions options = null)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
parameters = new TokenValidationParameters();
|
||||
}
|
||||
|
||||
var tokenValidator = new CloudFoundryTokenValidator(options ?? new AuthServerOptions());
|
||||
parameters.ValidateAudience = false;
|
||||
parameters.ValidateIssuer = true;
|
||||
parameters.ValidateLifetime = true;
|
||||
parameters.IssuerValidator = tokenValidator.ValidateIssuer;
|
||||
parameters.AudienceValidator = tokenValidator.ValidateAudience;
|
||||
|
||||
var tkr = new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates);
|
||||
parameters.IssuerSigningKeyResolver = tkr.ResolveSigningKey;
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the time at which a token was issued
|
||||
/// </summary>
|
||||
/// <param name="payload">Contents of a JWT</param>
|
||||
/// <returns>The <see cref="DateTime"/> representation of a token's issued-at time</returns>
|
||||
public static DateTime GetIssueTime(JObject payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
}
|
||||
|
||||
var time = payload.Value<long>("iat");
|
||||
return ToAbsoluteUTC(time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves expiration time property (exp) in a <see cref="JObject"/>
|
||||
/// </summary>
|
||||
/// <param name="payload">Contents of a JWT</param>
|
||||
/// <returns>The <see cref="DateTime"/> representation of a token's expiration</returns>
|
||||
public static DateTime GetExpTime(JObject payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
}
|
||||
|
||||
var time = payload.Value<long>("exp");
|
||||
return ToAbsoluteUTC(time);
|
||||
}
|
||||
|
||||
private static DateTime ToAbsoluteUTC(long secondsPastEpoch)
|
||||
{
|
||||
return baseTime.AddSeconds(secondsPastEpoch);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,9 +30,9 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
{
|
||||
internal static ConcurrentDictionary<string, SecurityKey> Resolved { get; set; } = new ConcurrentDictionary<string, SecurityKey>();
|
||||
|
||||
private string _jwtKeyUrl;
|
||||
private HttpMessageHandler _httpHandler;
|
||||
private bool _validateCertificates;
|
||||
private readonly string _jwtKeyUrl;
|
||||
private readonly HttpMessageHandler _httpHandler;
|
||||
private readonly bool _validateCertificates;
|
||||
|
||||
public CloudFoundryTokenKeyResolver(string jwtKeyUrl, HttpMessageHandler httpHandler, bool validateCertificates)
|
||||
{
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public class CloudFoundryTokenValidator
|
||||
{
|
||||
private readonly AuthServerOptions _options;
|
||||
|
||||
public CloudFoundryTokenValidator(AuthServerOptions options = null)
|
||||
{
|
||||
_options = options ?? new AuthServerOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that a token was issued by UAA
|
||||
/// </summary>
|
||||
/// <param name="issuer">Token issuer</param>
|
||||
/// <param name="securityToken">[Not used] Token to validate</param>
|
||||
/// <param name="validationParameters">[Not used]</param>
|
||||
/// <returns>The issuer, if valid, else <see langword="null" /></returns>
|
||||
public virtual string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
|
||||
{
|
||||
if (issuer.Contains("uaa"))
|
||||
{
|
||||
return issuer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that a token was meant for approved audience(s)
|
||||
/// </summary>
|
||||
/// <param name="audiences">The list of audiences the token is valid for</param>
|
||||
/// <param name="securityToken">[Not used] The token being validated</param>
|
||||
/// <param name="validationParameters">[Not used]</param>
|
||||
/// <returns><see langword="true"/> if the audience matches the client id or any value in AdditionalAudiences</returns>
|
||||
public virtual bool ValidateAudience(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
|
||||
{
|
||||
foreach (string audience in audiences)
|
||||
{
|
||||
if (audience.Equals(_options.ClientId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_options.AdditionalAudiences != null)
|
||||
{
|
||||
bool found = _options.AdditionalAudiences.Any(x => x.Equals(audience));
|
||||
if (found)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method validates scopes provided in configuration,
|
||||
/// to perform scope based Authorization
|
||||
/// </summary>
|
||||
/// <param name="validJwt">JSON Web token</param>
|
||||
/// <returns>true if scopes validated</returns>
|
||||
protected virtual bool ValidateScopes(JwtSecurityToken validJwt)
|
||||
{
|
||||
if (_options.RequiredScopes == null || !_options.RequiredScopes.Any())
|
||||
{
|
||||
return true; // nocheck
|
||||
}
|
||||
|
||||
if (!validJwt.Claims.Any(x => x.Type.Equals("scope") || x.Type.Equals("authorities")))
|
||||
{
|
||||
return false; // no scopes at all
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
foreach (Claim claim in validJwt.Claims)
|
||||
{
|
||||
if (claim.Type.Equals("scope") || (claim.Type.Equals("authorities") && _options.RequiredScopes.Any(x => x.Equals(claim.Value))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,11 +13,10 @@
|
|||
// limitations under the License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public class OpenIDTokenResponse
|
||||
public class OpenIdTokenResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "id_token")]
|
||||
public string IdentityToken { get; set; }
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.CloudFoundryBase.Test")]
|
|
@ -0,0 +1,39 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\versions.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Base Security Provider for CloudFoundry</Description>
|
||||
<VersionPrefix>$(SteeltoeVersion)</VersionPrefix>
|
||||
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
|
||||
<Authors>Pivotal;dtillman</Authors>
|
||||
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||
<AssemblyName>Steeltoe.Security.Authentication.CloudFoundryBase</AssemblyName>
|
||||
<PackageId>Steeltoe.Security.Authentication.CloudFoundryBase</PackageId>
|
||||
<PackageTags>CloudFoundry;ASPNET;ASPNET Core;Security;OAuth2;SSO;OpenID</PackageTags>
|
||||
<PackageIconUrl>https://steeltoe.io/images/transparent.png</PackageIconUrl>
|
||||
<PackageProjectUrl>https://steeltoe.io</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\Steeltoe.Security.Authentication.CloudFoundryBase.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(JwtTokensVersion)" />
|
||||
<PackageReference Include="Steeltoe.CloudFoundry.ConnectorBase" Version="$(SteeltoeConnectorVersion)" />
|
||||
<PackageReference Include="Steeltoe.Common.Http" Version="$(SteeltoeCommonVersion)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
<Link>stylecop.json</Link>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Steeltoe.Common.Http;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public class TokenExchanger
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly AuthServerOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public TokenExchanger(AuthServerOptions options, HttpClient httpclient = null, ILogger logger = null)
|
||||
{
|
||||
_options = options;
|
||||
_httpClient = httpclient ?? HttpClientHelper.GetHttpClient(options.ValidateCertificates, options.ClientTimeout);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the HTTP call to exchange an authorization code for a token
|
||||
/// </summary>
|
||||
/// <param name="code">The auth code to exchange</param>
|
||||
/// <param name="targetUrl">The full address of the token endpoint</param>
|
||||
/// <param name="cancellationToken">Your CancellationToken</param>
|
||||
/// <returns>The response from the remote server</returns>
|
||||
public async Task<HttpResponseMessage> ExchangeCodeForToken(string code, string targetUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestParameters = AuthCodeTokenRequestParameters(code);
|
||||
HttpRequestMessage requestMessage = GetTokenRequestMessage(requestParameters, targetUrl);
|
||||
_logger?.LogDebug("Exchanging code {code} for token at {accessTokenUrl}", code, targetUrl);
|
||||
|
||||
HttpClientHelper.ConfigureCertificateValidatation(
|
||||
_options.ValidateCertificates,
|
||||
out SecurityProtocolType protocolType,
|
||||
out RemoteCertificateValidationCallback prevValidator);
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(_options.ValidateCertificates, protocolType, prevValidator);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Passes an authorization code to OAuth server, maps server's <see cref="OpenIdTokenResponse"/> mapped to <see cref="ClaimsIdentity"/>
|
||||
/// </summary>
|
||||
/// <param name="code">Auth code received after user logs in at remote server</param>
|
||||
/// <returns>The user's ClaimsIdentity</returns>
|
||||
public async Task<ClaimsIdentity> ExchangeAuthCodeForClaimsIdentity(string code)
|
||||
{
|
||||
HttpResponseMessage response = await ExchangeCodeForToken(code, _options.AuthorizationUrl, default(CancellationToken));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var tokens = await response.Content.ReadAsJsonAsync<OpenIdTokenResponse>();
|
||||
#if DEBUG
|
||||
_logger?.LogTrace("Identity token received: {identityToken}", tokens.IdentityToken);
|
||||
_logger?.LogTrace("Access token received: {accessToken}", tokens.AccessToken);
|
||||
#endif
|
||||
JwtSecurityToken securityToken = new JwtSecurityToken(tokens.IdentityToken);
|
||||
|
||||
return BuildIdentityWithClaims(securityToken.Claims, tokens.Scope, tokens.AccessToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogError("Failed call to exchange code for token : " + response.StatusCode);
|
||||
_logger?.LogWarning(response.ReasonPhrase);
|
||||
_logger?.LogInformation(await response.Content.ReadAsStringAsync());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an access token using client_credentials grant
|
||||
/// </summary>
|
||||
/// <param name="targetUrl">full address of the token endpoint at the auth server</param>
|
||||
/// <returns>HttpResponse from the auth server</returns>
|
||||
public async Task<HttpResponseMessage> GetAccessTokenWithClientCredentials(string targetUrl)
|
||||
{
|
||||
HttpRequestMessage requestMessage = GetTokenRequestMessage(ClientCredentialsTokenRequestParameters(), targetUrl);
|
||||
|
||||
HttpClientHelper.ConfigureCertificateValidatation(_options.ValidateCertificates, out SecurityProtocolType protocolType, out RemoteCertificateValidationCallback prevValidator);
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(_options.ValidateCertificates, protocolType, prevValidator);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an <see cref="HttpRequestMessage"/> that will POST with the params to the target
|
||||
/// </summary>
|
||||
/// <param name="parameters">Body of the request to send</param>
|
||||
/// <param name="targetUrl">Location to send the request</param>
|
||||
/// <returns>A request primed for receiving a token</returns>
|
||||
internal HttpRequestMessage GetTokenRequestMessage(List<KeyValuePair<string, string>> parameters, string targetUrl)
|
||||
{
|
||||
var requestContent = new FormUrlEncodedContent(parameters);
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, targetUrl);
|
||||
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
requestMessage.Content = requestContent;
|
||||
return requestMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets request parameters for authorization_code Token request
|
||||
/// </summary>
|
||||
/// <param name="code">Authorization code to be exchanged for token</param>
|
||||
/// <returns>Content for HTTP request</returns>
|
||||
internal List<KeyValuePair<string, string>> AuthCodeTokenRequestParameters(string code)
|
||||
{
|
||||
var parms = CommonTokenRequestParams();
|
||||
parms.Add(new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsRedirectUri, _options.CallbackUrl));
|
||||
parms.Add(new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsCode, code));
|
||||
parms.Add(new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsGrantType, OpenIdConnectGrantTypes.AuthorizationCode));
|
||||
|
||||
return parms;
|
||||
}
|
||||
|
||||
internal List<KeyValuePair<string, string>> ClientCredentialsTokenRequestParameters()
|
||||
{
|
||||
var parms = CommonTokenRequestParams();
|
||||
parms.Add(new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsGrantType, OpenIdConnectGrantTypes.ClientCredentials));
|
||||
|
||||
return parms;
|
||||
}
|
||||
|
||||
internal List<KeyValuePair<string, string>> CommonTokenRequestParams()
|
||||
{
|
||||
var scopes = "openid " + _options.AdditionalTokenScopes;
|
||||
if (_options.RequiredScopes != null)
|
||||
{
|
||||
scopes = string.Join(" ", scopes, _options.RequiredScopes);
|
||||
}
|
||||
|
||||
return new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsClientId, _options.ClientId),
|
||||
new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsClientSecret, _options.ClientSecret),
|
||||
new KeyValuePair<string, string>(CloudFoundryDefaults.ParamsScope, scopes)
|
||||
};
|
||||
}
|
||||
|
||||
internal ClaimsIdentity BuildIdentityWithClaims(IEnumerable<Claim> claims, string tokenScopes, string accessToken)
|
||||
{
|
||||
#if DEBUG
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
_logger?.LogTrace(claim.Type + " : " + claim.Value);
|
||||
}
|
||||
#endif
|
||||
var claimsId = new ClaimsIdentity(_options.SignInAsAuthenticationType);
|
||||
|
||||
string userName = claims.First(c => c.Type == "user_name").Value;
|
||||
string email = claims.First(c => c.Type == "email").Value;
|
||||
string userId = claims.First(c => c.Type == "user_id").Value;
|
||||
|
||||
claimsId.AddClaims(new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||
new Claim(ClaimTypes.Name, userName),
|
||||
new Claim(ClaimTypes.Email, email),
|
||||
});
|
||||
|
||||
var additionalScopes = tokenScopes.Split(' ').Where(s => s != "openid");
|
||||
foreach (var scope in additionalScopes)
|
||||
{
|
||||
claimsId.AddClaim(new Claim("scope", scope));
|
||||
}
|
||||
|
||||
claimsId.AddClaim(new Claim(ClaimTypes.Authentication, accessToken));
|
||||
|
||||
return claimsId;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,9 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
|
@ -30,31 +28,13 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
|
||||
if (si != null)
|
||||
{
|
||||
options.JwtKeyUrl = si.AuthDomain + CloudFoundryDefaults.JwtTokenKey;
|
||||
options.JwtKeyUrl = si.AuthDomain + CloudFoundryDefaults.JwtTokenUri;
|
||||
}
|
||||
|
||||
jwtOptions.ClaimsIssuer = options.ClaimsIssuer;
|
||||
jwtOptions.BackchannelHttpHandler = CloudFoundryHelper.GetBackChannelHandler(options.ValidateCertificates);
|
||||
jwtOptions.TokenValidationParameters = GetTokenValidationParameters(jwtOptions.TokenValidationParameters, options.JwtKeyUrl, jwtOptions.BackchannelHttpHandler, options.ValidateCertificates);
|
||||
jwtOptions.TokenValidationParameters = CloudFoundryHelper.GetTokenValidationParameters(jwtOptions.TokenValidationParameters, options.JwtKeyUrl, jwtOptions.BackchannelHttpHandler, options.ValidateCertificates);
|
||||
jwtOptions.SaveToken = options.SaveToken;
|
||||
}
|
||||
|
||||
internal static TokenValidationParameters GetTokenValidationParameters(TokenValidationParameters parameters, string keyUrl, HttpMessageHandler handler, bool validateCertificates)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
parameters = new TokenValidationParameters();
|
||||
}
|
||||
|
||||
parameters.ValidateAudience = false;
|
||||
parameters.ValidateIssuer = true;
|
||||
parameters.ValidateLifetime = true;
|
||||
parameters.IssuerValidator = CloudFoundryTokenValidator.ValidateIssuer;
|
||||
|
||||
var tkr = new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates);
|
||||
parameters.IssuerSigningKeyResolver = tkr.ResolveSigningKey;
|
||||
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
{
|
||||
string authURL = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
ClaimsIssuer = CloudFoundryDefaults.AuthenticationScheme;
|
||||
JwtKeyUrl = authURL + CloudFoundryDefaults.JwtTokenKey;
|
||||
JwtKeyUrl = authURL + CloudFoundryDefaults.JwtTokenUri;
|
||||
SaveToken = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
{
|
||||
public class CloudFoundryOAuthHandler : OAuthHandler<CloudFoundryOAuthOptions>
|
||||
{
|
||||
private ILogger<CloudFoundryOAuthHandler> _logger;
|
||||
private readonly ILogger<CloudFoundryOAuthHandler> _logger;
|
||||
|
||||
public CloudFoundryOAuthHandler(
|
||||
IOptionsMonitor<CloudFoundryOAuthOptions> options,
|
||||
|
@ -70,30 +70,6 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
return request;
|
||||
}
|
||||
|
||||
protected internal virtual Dictionary<string, string> GetTokenRequestParameters(string code, string redirectUri)
|
||||
{
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{ "client_id", Options.ClientId },
|
||||
{ "redirect_uri", redirectUri },
|
||||
{ "client_secret", Options.ClientSecret },
|
||||
{ "code", code },
|
||||
{ "grant_type", "authorization_code" },
|
||||
};
|
||||
}
|
||||
|
||||
protected internal virtual HttpRequestMessage GetTokenRequestMessage(string code, string redirectUri)
|
||||
{
|
||||
var tokenRequestParameters = GetTokenRequestParameters(code, redirectUri);
|
||||
|
||||
var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
|
||||
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
requestMessage.Content = requestContent;
|
||||
return requestMessage;
|
||||
}
|
||||
|
||||
protected internal string GetEncoded(string user, string password)
|
||||
{
|
||||
if (user == null)
|
||||
|
@ -116,25 +92,13 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
|
||||
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
|
||||
{
|
||||
_logger?.LogDebug("ExchangeCodeAsync({code},{redirectUri})", code, redirectUri);
|
||||
_logger?.LogDebug("ExchangeCodeAsync({code}, {redirectUri})", code, redirectUri);
|
||||
|
||||
HttpRequestMessage requestMessage = GetTokenRequestMessage(code, redirectUri);
|
||||
HttpClient client = GetHttpClient();
|
||||
var options = Options.BaseOptions();
|
||||
options.CallbackUrl = redirectUri;
|
||||
|
||||
HttpClientHelper.ConfigureCertificateValidatation(
|
||||
Options.ValidateCertificates,
|
||||
out SecurityProtocolType prevProtocols,
|
||||
out RemoteCertificateValidationCallback prevValidator);
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
response = await client.SendAsync(requestMessage, Context.RequestAborted);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(Options.ValidateCertificates, prevProtocols, prevValidator);
|
||||
}
|
||||
var tEx = new TokenExchanger(options, GetHttpClient());
|
||||
HttpResponseMessage response = await tEx.ExchangeCodeForToken(code, Options.TokenEndpoint, Context.RequestAborted);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
|
@ -210,9 +174,9 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
|
||||
var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "response_type", "code" },
|
||||
{ "client_id", Options.ClientId },
|
||||
{ "redirect_uri", redirectUri }
|
||||
{ CloudFoundryDefaults.ParamsResponseType, "code" },
|
||||
{ CloudFoundryDefaults.ParamsClientId, Options.ClientId },
|
||||
{ CloudFoundryDefaults.ParamsRedirectUri, redirectUri }
|
||||
};
|
||||
|
||||
AddQueryString(queryStrings, properties, "scope", scope);
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
ClaimsIssuer = CloudFoundryDefaults.AuthenticationScheme;
|
||||
ClientId = CloudFoundryDefaults.ClientId;
|
||||
ClientSecret = CloudFoundryDefaults.ClientSecret;
|
||||
CallbackPath = new PathString("/signin-cloudfoundry");
|
||||
CallbackPath = new PathString(CloudFoundryDefaults.CallbackPath);
|
||||
AuthorizationEndpoint = authURL + CloudFoundryDefaults.AuthorizationUri;
|
||||
TokenEndpoint = authURL + CloudFoundryDefaults.AccessTokenUri;
|
||||
UserInformationEndpoint = authURL + CloudFoundryDefaults.UserInfoUri;
|
||||
|
@ -52,5 +52,16 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
|
||||
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
}
|
||||
|
||||
internal AuthServerOptions BaseOptions()
|
||||
{
|
||||
return new AuthServerOptions
|
||||
{
|
||||
ClientId = ClientId,
|
||||
ClientSecret = ClientSecret,
|
||||
ValidateCertificates = ValidateCertificates,
|
||||
AuthorizationUrl = AuthorizationEndpoint
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,14 +23,13 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OAuth" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Steeltoe.CloudFoundry.ConnectorBase" Version="$(SteeltoeConnectorVersion)" />
|
||||
<PackageReference Include="Steeltoe.Common.Http" Version="$(SteeltoeCommonVersion)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
<RootNamespace>Steeltoe.Security.Authentication.CloudFoundry</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
|
@ -38,4 +37,10 @@
|
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == ''">
|
||||
<ProjectReference Include="..\Steeltoe.Security.Authentication.CloudFoundryBase\Steeltoe.Security.Authentication.CloudFoundryBase.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == 'True'">
|
||||
<PackageReference Include="Steeltoe.Security.Authentication.CloudFoundryBase" Version="$(SteeltoeVersion)$(SteeltoeVersionSuffix)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Owin;
|
||||
using Steeltoe.CloudFoundry.Connector;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public static class CloudFoundryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures and adds <see cref="OpenIdConnectAuthenticationMiddleware" /> to the OWIN request pipeline />
|
||||
/// </summary>
|
||||
/// <param name="appBuilder">Your OWIN AppBuilder</param>
|
||||
/// <param name="configuration">Your application configuration</param>
|
||||
/// <param name="authenticationType">The identifier for this authentication type. MUST match the value used in your authentication challenge.</param>
|
||||
/// <param name="loggerFactory">For logging with SSO interactions</param>
|
||||
/// <returns>Your <see cref="IAppBuilder"/></returns>
|
||||
public static IAppBuilder UseCloudFoundryOpenIdConnect(this IAppBuilder appBuilder, IConfiguration configuration, string authenticationType = CloudFoundryDefaults.AuthenticationScheme, LoggerFactory loggerFactory = null)
|
||||
{
|
||||
if (appBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(appBuilder));
|
||||
}
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
// get options with defaults
|
||||
var cloudFoundryOptions = new OpenIdConnectOptions(authenticationType, loggerFactory);
|
||||
|
||||
// get and apply config from application
|
||||
var securitySection = configuration.GetSection(CloudFoundryDefaults.SECURITY_CLIENT_SECTION_PREFIX);
|
||||
securitySection.Bind(cloudFoundryOptions);
|
||||
|
||||
// get and apply service binding info
|
||||
SsoServiceInfo info = configuration.GetSingletonServiceInfo<SsoServiceInfo>();
|
||||
OpenIdConnectConfigurer.Configure(info, cloudFoundryOptions);
|
||||
|
||||
return appBuilder.Use(typeof(OpenIdConnectAuthenticationMiddleware), appBuilder, cloudFoundryOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable SA1403 // File may only contain a single namespace
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
#pragma warning restore SA1403 // File may only contain a single namespace
|
||||
{
|
||||
#pragma warning disable SA1402 // File may only contain a single class
|
||||
public static class CloudFoundryExtensions
|
||||
#pragma warning restore SA1402 // File may only contain a single class
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures and adds JWT bearer token middleware to the OWIN request pipeline
|
||||
/// </summary>
|
||||
/// <param name="appBuilder">Your OWIN AppBuilder</param>
|
||||
/// <param name="configuration">Your application configuration</param>
|
||||
/// <param name="logger">Include for diagnostic logging during app start</param>
|
||||
/// <returns>Your <see cref="IAppBuilder"/></returns>
|
||||
public static IAppBuilder UseCloudFoundryJwtBearerAuthentication(this IAppBuilder appBuilder, IConfiguration configuration, ILogger logger = null)
|
||||
{
|
||||
if (appBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(appBuilder));
|
||||
}
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
// get options with defaults
|
||||
var cloudFoundryOptions = new CloudFoundryJwtBearerAuthenticationOptions();
|
||||
|
||||
// get and apply config from application
|
||||
var securitySection = configuration.GetSection(CloudFoundryDefaults.SECURITY_CLIENT_SECTION_PREFIX);
|
||||
securitySection.Bind(cloudFoundryOptions);
|
||||
|
||||
// get and apply service binding info
|
||||
SsoServiceInfo si = configuration.GetSingletonServiceInfo<SsoServiceInfo>();
|
||||
CloudFoundryJwtOwinConfigurer.Configure(si, cloudFoundryOptions);
|
||||
|
||||
// REVIEW: return without adding auth middleware if no service binding was found... !?
|
||||
// - presumably written this way to support local development, but seems like a bad idea
|
||||
// - added option to disable, but leaving behavior to default this way, for now, to avoid a breaking change
|
||||
if (si == null && cloudFoundryOptions.SkipAuthIfNoBoundSSOService)
|
||||
{
|
||||
logger?.LogWarning("SSO Service binding not detected, JWT Bearer middleware has not been added!");
|
||||
logger?.LogInformation("To include JWT Bearer middleware when bindings aren't found, set security:oauth2:client:SkipAuthIfNoBoundSSOService=false");
|
||||
return appBuilder;
|
||||
}
|
||||
|
||||
return appBuilder.UseJwtBearerAuthentication(cloudFoundryOptions);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.Owin.Security.Jwt;
|
||||
using Owin;
|
||||
using Steeltoe.CloudFoundry.Connector;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public static class CloudFoundryJwtBearerExtensions
|
||||
{
|
||||
public static IAppBuilder UseCloudFoundryJwtBearerAuthentication(this IAppBuilder app, IConfiguration config)
|
||||
{
|
||||
var cloudFoundryOptions = new CloudFoundryJwtBearerAuthenticationOptions();
|
||||
var securitySection = config.GetSection(CloudFoundryDefaults.SECURITY_CLIENT_SECTION_PREFIX);
|
||||
securitySection.Bind(cloudFoundryOptions);
|
||||
|
||||
SsoServiceInfo si = config.GetSingletonServiceInfo<SsoServiceInfo>();
|
||||
if (si == null)
|
||||
{
|
||||
return app;
|
||||
}
|
||||
|
||||
var jwtTokenUrl = si.AuthDomain + CloudFoundryDefaults.JwtTokenKey;
|
||||
var httpMessageHandler = CloudFoundryHelper.GetBackChannelHandler(cloudFoundryOptions.ValidateCertificates);
|
||||
var tokenValidationParameters = GetTokenValidationParameters(jwtTokenUrl, httpMessageHandler, cloudFoundryOptions.ValidateCertificates);
|
||||
|
||||
return app.UseJwtBearerAuthentication(
|
||||
new JwtBearerAuthenticationOptions
|
||||
{
|
||||
TokenValidationParameters = tokenValidationParameters,
|
||||
});
|
||||
}
|
||||
|
||||
internal static TokenValidationParameters GetTokenValidationParameters(string keyUrl, HttpMessageHandler handler, bool validateCertificates)
|
||||
{
|
||||
var parameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = false,
|
||||
ValidateIssuer = true,
|
||||
ValidateLifetime = true,
|
||||
IssuerValidator = CloudFoundryTokenValidator.ValidateIssuer
|
||||
};
|
||||
|
||||
var tkr = new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates);
|
||||
parameters.IssuerSigningKeyResolver = tkr.ResolveSigningKey;
|
||||
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.Owin.Security.Jwt;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public static class CloudFoundryJwtOwinConfigurer
|
||||
{
|
||||
internal static void Configure(SsoServiceInfo si, JwtBearerAuthenticationOptions jwtOptions, CloudFoundryJwtBearerAuthenticationOptions options)
|
||||
{
|
||||
if (jwtOptions == null || options == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (si != null)
|
||||
{
|
||||
options.JwtKeyUrl = si.AuthDomain + CloudFoundryDefaults.JwtTokenKey;
|
||||
}
|
||||
|
||||
// jwtOptions.ClaimsIssuer = options.ClaimsIssuer;
|
||||
// jwtOptions.BackchannelHttpHandler = CloudFoundryHelper.GetBackChannelHandler(options.ValidateCertificates);
|
||||
// jwtOptions.TokenValidationParameters = GetTokenValidationParameters(jwtOptions.TokenValidationParameters, options.JwtKeyUrl, jwtOptions.BackchannelHttpHandler, options.ValidateCertificates);
|
||||
// jwtOptions.SaveToken = options.SaveToken;
|
||||
}
|
||||
|
||||
internal static TokenValidationParameters GetTokenValidationParameters(TokenValidationParameters parameters, string keyUrl, HttpMessageHandler handler, bool validateCertificates)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
parameters = new TokenValidationParameters();
|
||||
}
|
||||
|
||||
parameters.ValidateAudience = false;
|
||||
parameters.ValidateIssuer = true;
|
||||
parameters.ValidateLifetime = true;
|
||||
parameters.IssuerValidator = CloudFoundryTokenValidator.ValidateIssuer;
|
||||
|
||||
var tkr = new CloudFoundryTokenKeyResolver(keyUrl, handler, validateCertificates);
|
||||
parameters.IssuerSigningKeyResolver = tkr.ResolveSigningKey;
|
||||
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,23 +12,34 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const string DefaultAuthenticationType = "PivotalSSO";
|
||||
|
||||
public const string EndPointOAuthAuthorize = "oauth/authorize";
|
||||
public const string EndPointOAuthToken = "oauth/token";
|
||||
|
||||
public const string ParamsClientID = "client_id";
|
||||
public const string ParamsClientSecret = "client_secret";
|
||||
public const string ParamsResponseType = "response_type";
|
||||
public const string ParamsScope = "scope";
|
||||
public const string ParamsRedirectUri = "redirect_uri";
|
||||
public const string ParamsGrantType = "grant_type";
|
||||
public const string ParamsTokenFormat = "token_format";
|
||||
public const string ParamsCode = "code";
|
||||
[Obsolete("Use CloudFoundryDefaults.AuthorizationUri instead")]
|
||||
public const string EndPointOAuthAuthorize = CloudFoundryDefaults.AuthorizationUri;
|
||||
[Obsolete("Use CloudFoundryDefaults.AccessTokenUri instead")]
|
||||
public const string EndPointOAuthToken = CloudFoundryDefaults.AccessTokenUri;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsClientID instead")]
|
||||
public const string ParamsClientID = CloudFoundryDefaults.ParamsClientId;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsClientSecret instead")]
|
||||
public const string ParamsClientSecret = CloudFoundryDefaults.ParamsClientSecret;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsResponseType instead")]
|
||||
public const string ParamsResponseType = CloudFoundryDefaults.ParamsResponseType;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsScope instead")]
|
||||
public const string ParamsScope = CloudFoundryDefaults.ParamsScope;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsRedirectUri instead")]
|
||||
public const string ParamsRedirectUri = CloudFoundryDefaults.ParamsRedirectUri;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsGrantType instead")]
|
||||
public const string ParamsGrantType = CloudFoundryDefaults.ParamsGrantType;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsTokenFormat instead")]
|
||||
public const string ParamsTokenFormat = CloudFoundryDefaults.ParamsTokenFormat;
|
||||
[Obsolete("Use CloudFoundryDefaults.ParamsCode instead")]
|
||||
public const string ParamsCode = CloudFoundryDefaults.ParamsCode;
|
||||
|
||||
public const string ScopeOpenID = "openid";
|
||||
public const string GrantTypeAuthorizationCode = "authorization_code";
|
||||
|
|
|
@ -21,9 +21,15 @@ namespace Steeltoe.Security.Authentication.CloudFoundry
|
|||
public CloudFoundryJwtBearerAuthenticationOptions()
|
||||
{
|
||||
string authURL = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
JwtKeyUrl = authURL + CloudFoundryDefaults.JwtTokenKey;
|
||||
JwtKeyUrl = authURL + CloudFoundryDefaults.JwtTokenUri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether auth middleware is added if no service binding is found
|
||||
/// </summary>
|
||||
/// <remarks>Is set to 'true' for compatibility with releases prior to Steeltoe 2.2.</remarks>
|
||||
public bool SkipAuthIfNoBoundSSOService { get; set; } = true;
|
||||
|
||||
public string JwtKeyUrl { get; set; }
|
||||
|
||||
public bool ValidateCertificates { get; set; } = true;
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
{
|
||||
public static class CloudFoundryJwtOwinConfigurer
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply service binding info to JWT options
|
||||
/// </summary>
|
||||
/// <param name="si">Info for bound SSO Service</param>
|
||||
/// <param name="options">Options to be updated</param>
|
||||
internal static void Configure(SsoServiceInfo si, CloudFoundryJwtBearerAuthenticationOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (si != null)
|
||||
{
|
||||
options.JwtKeyUrl = si.AuthDomain + CloudFoundryDefaults.JwtTokenUri;
|
||||
}
|
||||
|
||||
var backchannelHttpHandler = CloudFoundryHelper.GetBackChannelHandler(options.ValidateCertificates);
|
||||
options.TokenValidationParameters = CloudFoundryHelper.GetTokenValidationParameters(options.TokenValidationParameters, options.JwtKeyUrl, backchannelHttpHandler, options.ValidateCertificates);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public class OpenIDConnectOptions : AuthenticationOptions
|
||||
{
|
||||
public OpenIDConnectOptions()
|
||||
: base(Constants.DefaultAuthenticationType)
|
||||
{
|
||||
Description.Caption = Constants.DefaultAuthenticationType;
|
||||
CallbackPath = new PathString("/signin-oidc");
|
||||
AuthenticationMode = AuthenticationMode.Passive;
|
||||
}
|
||||
|
||||
public PathString CallbackPath { get; set; }
|
||||
|
||||
public string AuthDomain { get; set; }
|
||||
|
||||
public string ClientID { get; set; }
|
||||
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
public string AppHost { get; set; }
|
||||
|
||||
public int AppPort { get; set; }
|
||||
|
||||
public string SignInAsAuthenticationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional scopes beyond 'openid' when requesting tokens
|
||||
/// </summary>
|
||||
public string AdditionalScopes { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to validate SSO server certificate
|
||||
/// </summary>
|
||||
public bool ValidateCertificates { get; set; } = true;
|
||||
|
||||
public string TokenInfoUrl => AuthDomain + "/check_token";
|
||||
|
||||
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
|
||||
}
|
||||
}
|
|
@ -13,12 +13,14 @@
|
|||
// limitations under the License.
|
||||
|
||||
using Owin;
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public static class OpenIDConnectExtension
|
||||
{
|
||||
[Obsolete("Use app.UseCloudFoundryOpenIdConnect instead")]
|
||||
public static IAppBuilder UseOpenIDConnect(this IAppBuilder app, OpenIDConnectOptions options)
|
||||
{
|
||||
// In order to talk to Pivotal SSO tile, we must limit the TLS defaults used by the service
|
|
@ -12,20 +12,12 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public static class CloudFoundryTokenValidator
|
||||
[Obsolete("This class will be removed in a future release, use Steeltoe.Security.Authentication.CloudFoundry.OpenIdTokenResponse instead.")]
|
||||
public class OpenIDTokenResponse : OpenIdTokenResponse
|
||||
{
|
||||
public static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
|
||||
{
|
||||
if (issuer.Contains("uaa"))
|
||||
{
|
||||
return issuer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,21 +12,31 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Infrastructure;
|
||||
using Microsoft.Owin.Security;
|
||||
using Microsoft.Owin.Security.Infrastructure;
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public class OpenIDConnectAuthenticationHandler : AuthenticationHandler<OpenIDConnectOptions>
|
||||
public class OpenIdConnectAuthenticationHandler : AuthenticationHandler<OpenIdConnectOptions>
|
||||
{
|
||||
/*
|
||||
* Invoked for every request. As this is passive middleware, we only want to branch off
|
||||
* the authentication flow if the request being invoked is the callback path we gave as a redirect
|
||||
* uri to the auth flow when invoking the IDP
|
||||
*/
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public OpenIdConnectAuthenticationHandler(ILogger logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked for every request.As this is passive middleware, we only want to branch off
|
||||
/// the authentication flow if the request being invoked is the callback path we gave as a redirect
|
||||
/// uri to the auth flow when invoking the IDP
|
||||
/// </summary>
|
||||
/// <returns>A bool used to determine whether or not to continue down the OWIN pipline</returns>
|
||||
public override async Task<bool> InvokeAsync()
|
||||
{
|
||||
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
|
||||
|
@ -48,19 +58,28 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
|||
|
||||
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
|
||||
{
|
||||
var code = Request.Query["code"];
|
||||
Debug.WriteLine("Received an authorization code from IDP: " + code);
|
||||
Debug.WriteLine("== exchanging for token ==");
|
||||
if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// ASP.Net Identity requires the NameIdentitifer field to be set or it won't
|
||||
// accept the external login (AuthenticationManagerExtensions.GetExternalLoginInfo)
|
||||
var identity = await TokenExchanger.ExchangeCodeForToken(code, Options);
|
||||
var code = Request.Query["code"];
|
||||
if (code == null)
|
||||
{
|
||||
var error = Request.Query["error"];
|
||||
var error_description = Request.Query["error_description"];
|
||||
_logger?.LogError("No auth code detected in SSO response. Error: {error}, Description: {error_description}", error, error_description);
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Received an authorization code from IDP: " + code);
|
||||
_logger?.LogInformation("== exchanging auth code for token ==");
|
||||
|
||||
var exchanger = new TokenExchanger(Options.AsAuthServerOptions(HostInfoFromRequest(Request) + Options.CallbackPath), null, _logger);
|
||||
var identity = await exchanger.ExchangeAuthCodeForClaimsIdentity(code);
|
||||
|
||||
var properties = Options.StateDataFormat.Unprotect(Request.Query["state"]);
|
||||
|
||||
// return Task.FromResult(new AuthenticationTicket(identity, properties));
|
||||
var ticket = new AuthenticationTicket(identity, properties);
|
||||
return ticket;
|
||||
return new AuthenticationTicket(identity, properties);
|
||||
}
|
||||
|
||||
protected override Task InitializeCoreAsync()
|
||||
|
@ -85,15 +104,27 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
|||
state.RedirectUri = Request.Uri.ToString();
|
||||
}
|
||||
|
||||
var stateString = Options.StateDataFormat.Protect(state);
|
||||
Debug.WriteLine("Challenge redirecting to IDP...");
|
||||
var idpRedirectUri = UriUtility.CalculateFullRedirectUri(Options, Request);
|
||||
_logger?.LogInformation("Redirecting to Identity Provider at {idpRedirectUri}", idpRedirectUri);
|
||||
|
||||
var idpRedirectUri = UriUtility.CalculateFullRedirectUri(Options);
|
||||
var stateString = Options.StateDataFormat.Protect(state);
|
||||
Response.Redirect(WebUtilities.AddQueryString(idpRedirectUri, "state", stateString));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<object>(null);
|
||||
}
|
||||
|
||||
private string HostInfoFromRequest(IOwinRequest request)
|
||||
{
|
||||
return request.Scheme + Uri.SchemeDelimiter + request.Host;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single class
|
||||
[Obsolete("This class has been renamed OpenIdConnectAuthenticationHandler")]
|
||||
public class OpenIDConnectAuthenticationHandler : OpenIdConnectAuthenticationHandler
|
||||
#pragma warning restore SA1402 // File may only contain a single class
|
||||
{
|
||||
}
|
||||
}
|
|
@ -18,12 +18,13 @@ using Microsoft.Owin.Security.DataHandler;
|
|||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Microsoft.Owin.Security.Infrastructure;
|
||||
using Owin;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public class OpenIDConnectAuthenticationMiddleware : AuthenticationMiddleware<OpenIDConnectOptions>
|
||||
public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware<OpenIdConnectOptions>
|
||||
{
|
||||
public OpenIDConnectAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, OpenIDConnectOptions options)
|
||||
public OpenIdConnectAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, OpenIdConnectOptions options)
|
||||
: base(next, options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Options.SignInAsAuthenticationType))
|
||||
|
@ -34,16 +35,27 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
|||
if (options.StateDataFormat == null)
|
||||
{
|
||||
var dataProtector = app.CreateDataProtector(
|
||||
typeof(OpenIDConnectAuthenticationMiddleware).FullName,
|
||||
typeof(OpenIdConnectAuthenticationMiddleware).FullName,
|
||||
options.AuthenticationType);
|
||||
|
||||
options.StateDataFormat = new PropertiesDataFormat(dataProtector);
|
||||
}
|
||||
}
|
||||
|
||||
protected override AuthenticationHandler<OpenIDConnectOptions> CreateHandler()
|
||||
protected override AuthenticationHandler<OpenIdConnectOptions> CreateHandler()
|
||||
{
|
||||
return new OpenIdConnectAuthenticationHandler(Options.LoggerFactory?.CreateLogger("OpenIdConnectAuthenticationHandler"));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single class
|
||||
[Obsolete("This class has been renamed OpenIdConnectAuthenticationHandler")]
|
||||
public class OpenIDConnectAuthenticationMiddleware : OpenIdConnectAuthenticationMiddleware
|
||||
#pragma warning restore SA1402 // File may only contain a single class
|
||||
{
|
||||
public OpenIDConnectAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, OpenIDConnectOptions options)
|
||||
: base(next, app, options)
|
||||
{
|
||||
return new OpenIDConnectAuthenticationHandler();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public static class OpenIdConnectConfigurer
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply service binding info to an <see cref="OpenIdConnectOptions"/> instance
|
||||
/// </summary>
|
||||
/// <param name="si">Service binding information</param>
|
||||
/// <param name="options">OpenID options to be updated</param>
|
||||
internal static void Configure(SsoServiceInfo si, OpenIdConnectOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (si == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
options.AuthDomain = si.AuthDomain;
|
||||
options.ClientId = si.ClientId;
|
||||
options.ClientSecret = si.ClientSecret;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
public class OpenIdConnectOptions : AuthenticationOptions
|
||||
{
|
||||
public OpenIdConnectOptions(string authenticationType = CloudFoundryDefaults.DisplayName, LoggerFactory loggerFactory = null)
|
||||
: base(authenticationType)
|
||||
{
|
||||
Description.Caption = authenticationType;
|
||||
AuthenticationMode = AuthenticationMode.Passive;
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public PathString CallbackPath { get; set; } = new PathString(CloudFoundryDefaults.CallbackPath);
|
||||
|
||||
public string AuthDomain { get; set; } = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
|
||||
public string ClientId { get; set; } = CloudFoundryDefaults.ClientId;
|
||||
|
||||
[Obsolete("This property will be removed in a future release, use ClientId instead")]
|
||||
public string ClientID
|
||||
{
|
||||
get { return ClientId; }
|
||||
set { ClientId = value; }
|
||||
}
|
||||
|
||||
public string ClientSecret { get; set; } = CloudFoundryDefaults.ClientSecret;
|
||||
|
||||
public string AppHost { get; set; }
|
||||
|
||||
public int AppPort { get; set; }
|
||||
|
||||
public string SignInAsAuthenticationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional scopes beyond 'openid' when requesting tokens
|
||||
/// </summary>
|
||||
public string AdditionalScopes { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to validate SSO server certificate
|
||||
/// </summary>
|
||||
public bool ValidateCertificates { get; set; } = true;
|
||||
|
||||
public string TokenInfoUrl => AuthDomain + CloudFoundryDefaults.CheckTokenUri;
|
||||
|
||||
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
|
||||
|
||||
public AuthServerOptions AsAuthServerOptions(string callbackUrl = null)
|
||||
{
|
||||
return new AuthServerOptions
|
||||
{
|
||||
ClientId = ClientId,
|
||||
ClientSecret = ClientSecret,
|
||||
AdditionalTokenScopes = AdditionalScopes,
|
||||
ValidateCertificates = ValidateCertificates,
|
||||
SignInAsAuthenticationType = SignInAsAuthenticationType,
|
||||
AuthorizationUrl = AuthDomain + CloudFoundryDefaults.AccessTokenUri,
|
||||
CallbackUrl = callbackUrl ?? "https://" + AppHost + (AppPort != 0 ? ":" + AppPort : string.Empty) + CallbackPath
|
||||
};
|
||||
}
|
||||
|
||||
internal ILoggerFactory LoggerFactory;
|
||||
}
|
||||
|
||||
[Obsolete("This class is being renamed OpenIdConnectOptions")]
|
||||
#pragma warning disable SA1402 // File may only contain a single class
|
||||
public class OpenIDConnectOptions : OpenIdConnectOptions
|
||||
#pragma warning restore SA1402 // File may only contain a single class
|
||||
{
|
||||
public OpenIDConnectOptions(string authenticationType = "PivotalSSO")
|
||||
: base(authenticationType)
|
||||
{
|
||||
CallbackPath = new PathString("/signin-oidc");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.CloudFoundryOwin.Test")]
|
|
@ -28,15 +28,10 @@
|
|||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == ''">
|
||||
<ProjectReference Include="..\Steeltoe.Security.Authentication.CloudFoundryCore\Steeltoe.Security.Authentication.CloudFoundryCore.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == 'True'">
|
||||
<PackageReference Include="Steeltoe.Security.Authentication.CloudFoundryCore" Version="$(SteeltoeVersion)$(SteeltoeVersionSuffix)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
<RootNamespace>Steeltoe.Security.Authentication.CloudFoundry.Owin</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
|
@ -44,5 +39,10 @@
|
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == ''">
|
||||
<ProjectReference Include="..\Steeltoe.Security.Authentication.CloudFoundryBase\Steeltoe.Security.Authentication.CloudFoundryBase.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == 'True'">
|
||||
<PackageReference Include="Steeltoe.Security.Authentication.CloudFoundryBase" Version="$(SteeltoeVersion)$(SteeltoeVersionSuffix)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Steeltoe.Common.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
||||
{
|
||||
internal class TokenExchanger
|
||||
{
|
||||
internal static async Task<ClaimsIdentity> ExchangeCodeForToken(string code, OpenIDConnectOptions options)
|
||||
{
|
||||
string redirect_url = "https://" + options.AppHost;
|
||||
if (options.AppPort != 0)
|
||||
{
|
||||
redirect_url = redirect_url + ":" + options.AppPort + options.CallbackPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
redirect_url = redirect_url + options.CallbackPath;
|
||||
}
|
||||
|
||||
var hostName = options.AuthDomain;
|
||||
var pairs = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(Constants.ParamsClientID, options.ClientID),
|
||||
new KeyValuePair<string, string>(Constants.ParamsClientSecret, options.ClientSecret),
|
||||
new KeyValuePair<string, string>(Constants.ParamsGrantType, Constants.GrantTypeAuthorizationCode),
|
||||
new KeyValuePair<string, string>(Constants.ParamsRedirectUri, redirect_url),
|
||||
new KeyValuePair<string, string>(Constants.ParamsCode, code)
|
||||
};
|
||||
var content = new FormUrlEncodedContent(pairs);
|
||||
var targetUrl = hostName + "/" + Constants.EndPointOAuthToken;
|
||||
Debug.WriteLine("About to submit request for token to : " + targetUrl);
|
||||
foreach (var item in pairs)
|
||||
{
|
||||
Debug.WriteLine(item.Key + ": " + item.Value);
|
||||
}
|
||||
|
||||
HttpClientHelper.ConfigureCertificateValidatation(options.ValidateCertificates, out SecurityProtocolType protocolType, out RemoteCertificateValidationCallback prevValidator);
|
||||
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
var byteArray = Encoding.ASCII.GetBytes(options.ClientID + ":" + options.ClientSecret);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsync(targetUrl, content);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var tokens = await response.Content.ReadAsJsonAsync<OpenIDTokenResponse>();
|
||||
Debug.WriteLine("Identity token from IDP: " + tokens.IdentityToken);
|
||||
Debug.WriteLine("Access token from IDP: " + tokens.AccessToken);
|
||||
JwtSecurityToken securityToken = new JwtSecurityToken(tokens.IdentityToken);
|
||||
var claimsId = new ClaimsIdentity(options.SignInAsAuthenticationType);
|
||||
string userName = securityToken.Claims.First(c => c.Type == "user_name").Value;
|
||||
string email = securityToken.Claims.First(c => c.Type == "email").Value;
|
||||
string userId = securityToken.Claims.First(c => c.Type == "user_id").Value;
|
||||
foreach (var claim in securityToken.Claims)
|
||||
{
|
||||
Debug.WriteLine(claim.Type + " : " + claim.Value);
|
||||
}
|
||||
|
||||
claimsId.AddClaims(new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||
new Claim(ClaimTypes.Name, userName),
|
||||
new Claim(ClaimTypes.Email, email),
|
||||
});
|
||||
|
||||
var additionalScopes = tokens.Scope.Split(' ').Where(s => s != "openid");
|
||||
foreach (var scope in additionalScopes)
|
||||
{
|
||||
claimsId.AddClaim(new Claim("scope", scope));
|
||||
}
|
||||
|
||||
claimsId.AddClaim(new Claim(ClaimTypes.Authentication, tokens.AccessToken));
|
||||
|
||||
return claimsId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed call to exchange code for token : " + response.StatusCode);
|
||||
Debug.WriteLine(response.ReasonPhrase);
|
||||
string resultJson = await response.Content.ReadAsStringAsync();
|
||||
Debug.WriteLine(resultJson);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(options.ValidateCertificates, protocolType, prevValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Infrastructure;
|
||||
using System;
|
||||
|
||||
|
@ -19,28 +20,30 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Owin
|
|||
{
|
||||
internal static class UriUtility
|
||||
{
|
||||
internal static string CalculateFullRedirectUri(OpenIDConnectOptions options)
|
||||
/// <summary>
|
||||
/// Determine full redirect uri to send user to auth server, include a valid return path
|
||||
/// </summary>
|
||||
/// <param name="options">Auth configuration</param>
|
||||
/// <param name="request">HTTP Request information, for generating a valid return paht</param>
|
||||
/// <returns>A URL with enough info for the auth server to identify the app and return the user to the right location after auth</returns>
|
||||
internal static string CalculateFullRedirectUri(OpenIdConnectOptions options, IOwinRequest request)
|
||||
{
|
||||
var uri = options.AuthDomain + "/" + Constants.EndPointOAuthAuthorize;
|
||||
var uri = options.AuthDomain + CloudFoundryDefaults.AuthorizationUri;
|
||||
|
||||
var queryString = WebUtilities.AddQueryString(uri, Constants.ParamsClientID, options.ClientID);
|
||||
queryString = WebUtilities.AddQueryString(queryString, Constants.ParamsResponseType, "code");
|
||||
queryString = WebUtilities.AddQueryString(queryString, Constants.ParamsScope, $"{Constants.ScopeOpenID} {options.AdditionalScopes}");
|
||||
queryString = WebUtilities.AddQueryString(queryString, Constants.ParamsRedirectUri, DetermineRedirectUri(options));
|
||||
var queryString = WebUtilities.AddQueryString(uri, CloudFoundryDefaults.ParamsClientId, options.ClientId);
|
||||
queryString = WebUtilities.AddQueryString(queryString, CloudFoundryDefaults.ParamsResponseType, "code");
|
||||
queryString = WebUtilities.AddQueryString(queryString, CloudFoundryDefaults.ParamsScope, $"{Constants.ScopeOpenID} {options.AdditionalScopes}");
|
||||
queryString = WebUtilities.AddQueryString(queryString, CloudFoundryDefaults.ParamsRedirectUri, DetermineRedirectUri(options, request));
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
private static string DetermineRedirectUri(OpenIDConnectOptions options)
|
||||
private static string DetermineRedirectUri(OpenIdConnectOptions options, IOwinRequest request)
|
||||
{
|
||||
// TODO: determine the right host name and port.
|
||||
var uri = "https://" + options.AppHost;
|
||||
if (options.AppPort > 0 && options.AppPort != 443)
|
||||
{
|
||||
uri = uri + ":" + options.AppPort.ToString();
|
||||
}
|
||||
|
||||
return uri + options.CallbackPath;
|
||||
return request.Scheme +
|
||||
Uri.SchemeDelimiter +
|
||||
request.Host +
|
||||
options.CallbackPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,55 +12,50 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Steeltoe.Common.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
/// <summary>
|
||||
/// Get JWTs for WCF Clients
|
||||
/// </summary>
|
||||
public class CloudFoundryClientTokenResolver
|
||||
{
|
||||
public CloudFoundryOptions Options { get; internal protected set; }
|
||||
|
||||
public CloudFoundryClientTokenResolver(CloudFoundryOptions options)
|
||||
private readonly ILogger<CloudFoundryClientTokenResolver> _logger;
|
||||
private readonly TokenExchanger _tokenExchanger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudFoundryClientTokenResolver"/> class.
|
||||
/// This class can be used to get access tokens from an OAuth server
|
||||
/// </summary>
|
||||
/// <param name="options">Con</param>
|
||||
/// <param name="httpClient">For interacting with the OAuth server. A new instance will be created if not provided.</param>
|
||||
public CloudFoundryClientTokenResolver(CloudFoundryOptions options, HttpClient httpClient = null)
|
||||
{
|
||||
Options = options ?? throw new ArgumentNullException("Options are required");
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options), "Options are required");
|
||||
_tokenExchanger = new TokenExchanger(options, httpClient, options.LoggerFactory?.CreateLogger<TokenExchanger>());
|
||||
_logger = Options.LoggerFactory?.CreateLogger<CloudFoundryClientTokenResolver>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an access token using the client_credentials grant and the application's oauth credentials
|
||||
/// </summary>
|
||||
/// <returns>An access token</returns>
|
||||
public virtual async Task<string> GetAccessToken()
|
||||
{
|
||||
HttpRequestMessage requestMessage = GetTokenRequestMessage();
|
||||
|
||||
HttpClientHelper.ConfigureCertificateValidatation(Options.ValidateCertificates, out SecurityProtocolType protocolType, out RemoteCertificateValidationCallback prevValidator);
|
||||
|
||||
HttpClient client = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(Options.OAuthServiceUrl)
|
||||
};
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
response = await client.SendAsync(requestMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error getting access token:" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(Options.ValidateCertificates, protocolType, prevValidator);
|
||||
}
|
||||
HttpResponseMessage response = await _tokenExchanger.GetAccessTokenWithClientCredentials(Options.AuthorizationUrl + Options.AccessTokenEndpoint);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger?.LogTrace("Successfully retrieved access token");
|
||||
|
||||
var resp = await response.Content.ReadAsStringAsync();
|
||||
var payload = JObject.Parse(resp);
|
||||
|
||||
|
@ -68,54 +63,13 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogError("Failed to retrieve access token with HTTP Status: {HttpStatus}", response.StatusCode);
|
||||
_logger?.LogWarning("Access token retrieval failure response: {Message}", response.Content.ReadAsStringAsync());
|
||||
var error = "OAuth token endpoint failure: " + await Display(response);
|
||||
throw new Exception(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected internal virtual HttpRequestMessage GetTokenRequestMessage()
|
||||
{
|
||||
var tokenRequestParameters = GetTokenRequestParameters();
|
||||
|
||||
var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.AccessTokenEndpoint);
|
||||
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetEncoded(Options.ClientId, Options.ClientSecret));
|
||||
requestMessage.Content = requestContent;
|
||||
return requestMessage;
|
||||
}
|
||||
|
||||
protected internal virtual Dictionary<string, string> GetTokenRequestParameters()
|
||||
{
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{ "client_id", Options.ClientId },
|
||||
{ "client_secret", Options.ClientSecret },
|
||||
{ "response_type", "token" },
|
||||
{ "grant_type", "client_credentials" },
|
||||
{
|
||||
"scope", Options.RequiredScopes == null ? "openid" :
|
||||
string.Join(" ", Options.RequiredScopes)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected internal string GetEncoded(string user, string password)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
user = string.Empty;
|
||||
}
|
||||
|
||||
if (password == null)
|
||||
{
|
||||
password = string.Empty;
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(Encoding.ASCII.GetBytes(user + ":" + password));
|
||||
}
|
||||
|
||||
private static async Task<string> Display(HttpResponseMessage response)
|
||||
{
|
||||
var output = new StringBuilder();
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Steeltoe.CloudFoundry.Connector;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.ServiceModel;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
public static class CloudFoundryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="JwtAuthorizationManager"/> to a <see cref="ServiceHost"/>
|
||||
/// </summary>
|
||||
/// <param name="serviceHost">Your service to be secured with JWT Auth</param>
|
||||
/// <param name="configuration">Your application configuration, including VCAP_SERVICES</param>
|
||||
/// <param name="httpClient">Provide your own http client for interacting with the security server</param>
|
||||
/// <param name="loggerFactory">For logging within the library</param>
|
||||
/// <returns>Your service</returns>
|
||||
public static ServiceHost AddJwtAuthorization(this ServiceHost serviceHost, IConfiguration configuration, HttpClient httpClient = null, LoggerFactory loggerFactory = null)
|
||||
{
|
||||
if (serviceHost == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceHost));
|
||||
}
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
// get options with defaults
|
||||
var cloudFoundryOptions = new CloudFoundryOptions(loggerFactory);
|
||||
|
||||
// get and apply config from application
|
||||
var securitySection = configuration.GetSection(CloudFoundryDefaults.SECURITY_CLIENT_SECTION_PREFIX);
|
||||
securitySection.Bind(cloudFoundryOptions);
|
||||
|
||||
// get and apply service binding info
|
||||
SsoServiceInfo info = configuration.GetSingletonServiceInfo<SsoServiceInfo>();
|
||||
CloudFoundryOptionsConfigurer.Configure(info, cloudFoundryOptions);
|
||||
|
||||
var authManager = new JwtAuthorizationManager(cloudFoundryOptions);
|
||||
serviceHost.Authorization.ServiceAuthorizationManager = authManager;
|
||||
|
||||
return serviceHost;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,10 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
|
||||
|
@ -25,70 +22,38 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
{
|
||||
public static void OnTokenValidatedAddClaims(ClaimsIdentity identity, JwtSecurityToken jwt)
|
||||
{
|
||||
var identifier = GetId(identity);
|
||||
if (!string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, jwt.Issuer));
|
||||
}
|
||||
AddClaimIfNotNullOrEmpty(identity, "user_id", ClaimTypes.NameIdentifier, jwt.Issuer);
|
||||
AddClaimIfNotNullOrEmpty(identity, "given_name", ClaimTypes.GivenName, jwt.Issuer);
|
||||
AddClaimIfNotNullOrEmpty(identity, "family_name", ClaimTypes.Surname, jwt.Issuer);
|
||||
AddClaimIfNotNullOrEmpty(identity, "email", ClaimTypes.Email, jwt.Issuer);
|
||||
|
||||
var givenName = GetGivenName(identity);
|
||||
if (!string.IsNullOrEmpty(givenName))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.GivenName, givenName, ClaimValueTypes.String, jwt.Issuer));
|
||||
}
|
||||
|
||||
var familyName = GetFamilyName(identity);
|
||||
if (!string.IsNullOrEmpty(familyName))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Surname, familyName, ClaimValueTypes.String, jwt.Issuer));
|
||||
}
|
||||
|
||||
var email = GetEmail(identity);
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, jwt.Issuer));
|
||||
}
|
||||
|
||||
var name = GetName(identity);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, jwt.Issuer));
|
||||
}
|
||||
else
|
||||
if (!AddClaimIfNotNullOrEmpty(identity, "user_name", ClaimTypes.Name, jwt.Issuer))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Name, GetClientId(identity), ClaimValueTypes.String, jwt.Issuer));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetGivenName(IIdentity identity)
|
||||
/// <summary>
|
||||
/// Add a claim to the identity from another location, if found
|
||||
/// </summary>
|
||||
/// <param name="identity">The identity to investicate and modify</param>
|
||||
/// <param name="claimLocator">The claim we're trying to get</param>
|
||||
/// <param name="claimType">The claim we're trying to set</param>
|
||||
/// <param name="jwtIssuer">Issuer of the JWT</param>
|
||||
/// <returns>True if the claim was not null or empty (and was added)</returns>
|
||||
private static bool AddClaimIfNotNullOrEmpty(ClaimsIdentity identity, string claimLocator, string claimType, string jwtIssuer)
|
||||
{
|
||||
return GetClaim(identity, "given_name");
|
||||
var claimValue = GetClaim(identity, claimLocator);
|
||||
if (!string.IsNullOrEmpty(claimValue))
|
||||
{
|
||||
identity.AddClaim(new Claim(claimType, claimValue, ClaimValueTypes.String, jwtIssuer));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetFamilyName(IIdentity identity)
|
||||
{
|
||||
return GetClaim(identity, "family_name");
|
||||
}
|
||||
|
||||
private static string GetEmail(IIdentity identity)
|
||||
{
|
||||
return GetClaim(identity, "email");
|
||||
}
|
||||
|
||||
private static string GetName(IIdentity identity)
|
||||
{
|
||||
return GetClaim(identity, "user_name");
|
||||
}
|
||||
|
||||
private static string GetId(IIdentity identity)
|
||||
{
|
||||
return GetClaim(identity, "user_id");
|
||||
}
|
||||
|
||||
private static string GetClientId(IIdentity identity)
|
||||
{
|
||||
return GetClaim(identity, "client_id");
|
||||
}
|
||||
private static string GetClientId(IIdentity identity) => GetClaim(identity, "client_id");
|
||||
|
||||
private static string GetClaim(IIdentity identity, string claim)
|
||||
{
|
||||
|
@ -106,35 +71,5 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
|
||||
return idClaim.Value;
|
||||
}
|
||||
|
||||
private static Claim[] GetClaims(IIdentity identity, string claim)
|
||||
{
|
||||
var claims = identity as ClaimsIdentity;
|
||||
if (claims == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var idClaims = claims.FindAll(claim);
|
||||
if (idClaims == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return idClaims.ToArray<Claim>();
|
||||
}
|
||||
|
||||
private string GetClaimWithFallback(IEnumerable<Claim> claims, params string[] claimTypes)
|
||||
{
|
||||
foreach (var claimType in claimTypes)
|
||||
{
|
||||
if (claims.Count(c => c.Type == claimType) > 0)
|
||||
{
|
||||
return claims.SingleOrDefault(c => c.Type == claimType).Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,47 +13,47 @@
|
|||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Steeltoe.CloudFoundry.Connector;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
public class CloudFoundryOptions
|
||||
public class CloudFoundryOptions : AuthServerOptions
|
||||
{
|
||||
public const string AUTHENTICATION_SCHEME = "CloudFoundry";
|
||||
public const string AUTHENTICATION_SCHEME = CloudFoundryDefaults.AuthenticationScheme;
|
||||
public const string OAUTH_AUTHENTICATION_SCHEME = "CloudFoundry.OAuth";
|
||||
|
||||
public string TokenInfoUrl { get; set; }
|
||||
public string TokenInfoUrl => AuthorizationUrl + CloudFoundryDefaults.CheckTokenUri;
|
||||
|
||||
public bool ValidateCertificates { get; set; }
|
||||
public bool ValidateAudience { get; set; } = true;
|
||||
|
||||
public bool ValidateAudience { get; set; }
|
||||
public bool ValidateIssuer { get; set; } = true;
|
||||
|
||||
public bool ValidateIssuer { get; set; }
|
||||
public bool ValidateLifetime { get; set; } = true;
|
||||
|
||||
public bool ValidateLifetime { get; set; }
|
||||
[Obsolete("This property will be removed in a future release. Use AuthorizationUrl instead")]
|
||||
public string OAuthServiceUrl { get => AuthorizationUrl; set => AuthorizationUrl = value; }
|
||||
|
||||
public string OAuthServiceUrl { get; set; }
|
||||
public string AuthorizationEndpoint { get; set; } = CloudFoundryDefaults.AuthorizationUri;
|
||||
|
||||
public string ClientId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets '/oauth/token'
|
||||
/// </summary>
|
||||
public string AccessTokenEndpoint { get; set; } = CloudFoundryDefaults.AccessTokenUri;
|
||||
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
public string AuthorizationEndpoint { get; set; } = "oauth/authorize";
|
||||
|
||||
public string AccessTokenEndpoint { get; set; } = "oauth/token";
|
||||
|
||||
public string UserInformationEndpoint { get; set; } = "userinfo";
|
||||
public string UserInformationEndpoint { get; set; } = CloudFoundryDefaults.UserInfoUri;
|
||||
|
||||
[Obsolete("This property will be removed in a future release. Use CloudFoundryDefaults.CheckTokenUri instead")]
|
||||
public string TokenInfoEndpoint { get; set; } = "check_token";
|
||||
|
||||
[Obsolete("This property will be removed in a future release. Use CloudFoundryDefaults.JwtTokenKey instead")]
|
||||
public string JwtKeyEndpoint { get; set; } = "token_key";
|
||||
|
||||
public string[] RequiredScopes { get; set; }
|
||||
|
||||
public string[] AdditionalAudiences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use app credentials or forward users's credentials
|
||||
/// </summary>
|
||||
|
@ -62,73 +62,99 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
|
||||
public TokenValidationParameters TokenValidationParameters { get; set; }
|
||||
|
||||
internal CloudFoundryTokenKeyResolver TokenKeyResolver { get; set; }
|
||||
internal Steeltoe.Security.Authentication.CloudFoundry.CloudFoundryTokenKeyResolver TokenKeyResolver { get; set; }
|
||||
|
||||
internal CloudFoundryTokenValidator TokenValidator { get; set; }
|
||||
internal CloudFoundryWcfTokenValidator TokenValidator { get; set; }
|
||||
|
||||
public CloudFoundryOptions()
|
||||
: this(System.Environment.GetEnvironmentVariable("sso_auth_domain"))
|
||||
internal readonly LoggerFactory LoggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloudFoundryOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">For logging within the library</param>
|
||||
public CloudFoundryOptions(LoggerFactory loggerFactory = null)
|
||||
{
|
||||
ClientId = System.Environment.GetEnvironmentVariable("sso_client_id");
|
||||
ClientSecret = System.Environment.GetEnvironmentVariable("sso_client_secret");
|
||||
LoggerFactory = loggerFactory;
|
||||
AuthorizationUrl = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
|
||||
// can't mark this constructor obsolete but want to make these go away
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("sso_auth_domain")) ||
|
||||
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("sso_client_id")) ||
|
||||
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("sso_client_secret")))
|
||||
{
|
||||
AuthorizationUrl = Environment.GetEnvironmentVariable("sso_auth_domain");
|
||||
ClientId = Environment.GetEnvironmentVariable("sso_client_id");
|
||||
ClientSecret = Environment.GetEnvironmentVariable("sso_client_secret");
|
||||
Console.Error.WriteLine("sso_* variables were detected in your environment! Future releases of Steeltoe will not uses them for configuration");
|
||||
Debug.WriteLine("sso_* variables were detected in your environment! Future releases of Steeltoe will not uses them for configuration");
|
||||
}
|
||||
|
||||
TokenKeyResolver = TokenKeyResolver ??
|
||||
new Steeltoe.Security.Authentication.CloudFoundry.CloudFoundryTokenKeyResolver(
|
||||
AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri,
|
||||
null,
|
||||
ValidateCertificates);
|
||||
TokenValidator = TokenValidator ?? new CloudFoundryWcfTokenValidator(this, LoggerFactory?.CreateLogger<CloudFoundryWcfTokenValidator>());
|
||||
}
|
||||
|
||||
public CloudFoundryOptions(string authDomain, string clientId, string clientSecret)
|
||||
: this(authDomain)
|
||||
[Obsolete("This constructor is expected to be removed in a future release. Please reach out if this constructor is important to you!")]
|
||||
public CloudFoundryOptions(string authDomain, string clientId, string clientSecret, LoggerFactory loggerFactory = null)
|
||||
: this(loggerFactory)
|
||||
{
|
||||
AuthorizationUrl = authDomain;
|
||||
ClientId = clientId;
|
||||
ClientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public CloudFoundryOptions(string authUrl)
|
||||
[Obsolete("This constructor is expected to be removed in a future release. Please reach out if this constructor is important to you!")]
|
||||
public CloudFoundryOptions(string authUrl, LoggerFactory loggerFactory = null)
|
||||
: this(loggerFactory)
|
||||
{
|
||||
// CallbackPath = new PathString("/signin-cloudfoundry");
|
||||
OAuthServiceUrl = authUrl;
|
||||
ValidateCertificates = false;
|
||||
ValidateAudience = true;
|
||||
ValidateIssuer = true;
|
||||
ValidateLifetime = true;
|
||||
TokenKeyResolver = TokenKeyResolver ?? new CloudFoundryTokenKeyResolver(this);
|
||||
TokenValidator = TokenValidator ?? new CloudFoundryTokenValidator(this);
|
||||
|
||||
TokenValidationParameters = GetTokenValidationParameters(this);
|
||||
AuthorizationUrl = authUrl;
|
||||
}
|
||||
|
||||
[Obsolete("This constructor is expected to be removed in a future release. Please reach out if this constructor is important to you!")]
|
||||
public CloudFoundryOptions(IConfiguration config)
|
||||
{
|
||||
// CloudFoundryDefaults.SECURITY_CLIENT_SECTION_PREFIX
|
||||
var securitySection = config.GetSection("security:oauth2:client");
|
||||
var securitySection = config.GetSection(CloudFoundryDefaults.SECURITY_CLIENT_SECTION_PREFIX);
|
||||
securitySection.Bind(this);
|
||||
|
||||
SsoServiceInfo info = config.GetSingletonServiceInfo<SsoServiceInfo>();
|
||||
|
||||
OAuthServiceUrl = info.AuthDomain;
|
||||
AuthorizationUrl = info.AuthDomain;
|
||||
ClientId = info.ClientId;
|
||||
ClientSecret = info.ClientSecret;
|
||||
TokenKeyResolver = TokenKeyResolver ?? new CloudFoundryTokenKeyResolver(this);
|
||||
TokenValidator = TokenValidator ?? new CloudFoundryTokenValidator(this);
|
||||
TokenValidationParameters = TokenValidationParameters ?? GetTokenValidationParameters(this);
|
||||
TokenKeyResolver = TokenKeyResolver ??
|
||||
new Steeltoe.Security.Authentication.CloudFoundry.CloudFoundryTokenKeyResolver(
|
||||
AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri,
|
||||
null,
|
||||
ValidateCertificates);
|
||||
TokenValidator = TokenValidator ?? new CloudFoundryWcfTokenValidator(this);
|
||||
TokenValidationParameters = TokenValidationParameters ?? GetTokenValidationParameters();
|
||||
}
|
||||
|
||||
internal static TokenValidationParameters GetTokenValidationParameters(CloudFoundryOptions options)
|
||||
internal TokenValidationParameters GetTokenValidationParameters()
|
||||
{
|
||||
if (options.TokenValidationParameters != null)
|
||||
if (TokenValidationParameters != null)
|
||||
{
|
||||
return options.TokenValidationParameters;
|
||||
return TokenValidationParameters;
|
||||
}
|
||||
|
||||
var parameters = new TokenValidationParameters();
|
||||
options.TokenKeyResolver = options.TokenKeyResolver ?? new CloudFoundryTokenKeyResolver(options);
|
||||
options.TokenValidator = options.TokenValidator ?? new CloudFoundryTokenValidator(options);
|
||||
options.TokenValidationParameters = parameters;
|
||||
TokenKeyResolver = TokenKeyResolver ??
|
||||
new Steeltoe.Security.Authentication.CloudFoundry.CloudFoundryTokenKeyResolver(
|
||||
AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri,
|
||||
null,
|
||||
ValidateCertificates);
|
||||
TokenValidator = TokenValidator ?? new CloudFoundryWcfTokenValidator(this, LoggerFactory?.CreateLogger<CloudFoundryWcfTokenValidator>());
|
||||
TokenValidationParameters = parameters;
|
||||
|
||||
parameters.ValidateAudience = options.ValidateAudience;
|
||||
parameters.ValidateIssuer = options.ValidateIssuer;
|
||||
parameters.ValidateLifetime = options.ValidateLifetime;
|
||||
|
||||
parameters.IssuerSigningKeyResolver = options.TokenKeyResolver.ResolveSigningKey;
|
||||
parameters.IssuerValidator = options.TokenValidator.ValidateIssuer;
|
||||
parameters.AudienceValidator = options.TokenValidator.ValidateAudience;
|
||||
parameters.ValidateAudience = ValidateAudience;
|
||||
parameters.AudienceValidator = TokenValidator.ValidateAudience;
|
||||
parameters.ValidateIssuer = ValidateIssuer;
|
||||
parameters.IssuerSigningKeyResolver = TokenKeyResolver.ResolveSigningKey;
|
||||
parameters.ValidateLifetime = ValidateLifetime;
|
||||
parameters.IssuerValidator = TokenValidator.ValidateIssuer;
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
internal static class CloudFoundryOptionsConfigurer
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply service binding info to an <see cref="CloudFoundryOptions"/> instance
|
||||
/// </summary>
|
||||
/// <param name="si">Service binding information</param>
|
||||
/// <param name="options">CloudFoundryOptions options to be updated</param>
|
||||
internal static void Configure(SsoServiceInfo si, CloudFoundryOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (si == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
options.AuthorizationUrl = si.AuthDomain;
|
||||
options.ClientId = si.ClientId;
|
||||
options.ClientSecret = si.ClientSecret;
|
||||
|
||||
var backchannelHttpHandler = CloudFoundryHelper.GetBackChannelHandler(options.ValidateCertificates);
|
||||
options.TokenValidationParameters = CloudFoundryHelper.GetTokenValidationParameters(options.TokenValidationParameters, options.AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri, backchannelHttpHandler, options.ValidateCertificates, options);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,16 +24,20 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
[Obsolete("This class will be removed in favor of Steeltoe.Security.Authentication.CloudFoundry.CloudFoundryTokenKeyResolver")]
|
||||
public class CloudFoundryTokenKeyResolver
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudFoundryOptions Options { get; internal protected set; }
|
||||
|
||||
public Dictionary<string, SecurityKey> Resolved { get; internal protected set; }
|
||||
|
||||
public CloudFoundryTokenKeyResolver(CloudFoundryOptions options)
|
||||
public CloudFoundryTokenKeyResolver(CloudFoundryOptions options, HttpClient httpClient = null)
|
||||
{
|
||||
Options = options ?? throw new ArgumentNullException("options null");
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
Resolved = new Dictionary<string, SecurityKey>();
|
||||
_httpClient = httpClient ?? HttpClientHelper.GetHttpClient(options.ValidateCertificates, options.ClientTimeout);
|
||||
}
|
||||
|
||||
public virtual IEnumerable<SecurityKey> ResolveSigningKey(string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters)
|
||||
|
@ -68,39 +72,36 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the token keys used by the OAuth server
|
||||
/// </summary>
|
||||
/// <returns><see cref="JsonWebKeySet"/></returns>
|
||||
/// <exception cref="ArgumentNullException">From the underlying HttpClient.SendAsync call - request is null</exception>
|
||||
/// <exception cref="InvalidOperationException">From the underlying HttpClient.SendAsync call - request already sent</exception>
|
||||
/// <exception cref="HttpRequestException">From the underlying HttpClient.SendAsync call - possibly network, DNS or certificate issues</exception>
|
||||
public virtual async Task<JsonWebKeySet> FetchKeySet()
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, Options.JwtKeyEndpoint);
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, Options.AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri);
|
||||
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
using (var handler = new HttpClientHandler())
|
||||
HttpClientHelper.ConfigureCertificateValidatation(
|
||||
Options.ValidateCertificates,
|
||||
out SecurityProtocolType protocolType,
|
||||
out RemoteCertificateValidationCallback prevValidator);
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
HttpClientHelper.ConfigureCertificateValidatation(Options.ValidateCertificates, out SecurityProtocolType protocolType, out RemoteCertificateValidationCallback prevValidator);
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(Options.ValidateCertificates, protocolType, prevValidator);
|
||||
}
|
||||
|
||||
HttpClient client = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri(Options.OAuthServiceUrl)
|
||||
};
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
response = await client.SendAsync(requestMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error getting keys to validate token:" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HttpClientHelper.RestoreCertificateValidation(Options.ValidateCertificates, protocolType, prevValidator);
|
||||
}
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
return GetJsonWebKeySet(result);
|
||||
}
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadAsJsonAsync<JsonWebKeySet>();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -12,11 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.ServiceModel.Web;
|
||||
|
@ -24,71 +23,54 @@ using System.Text.RegularExpressions;
|
|||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
public class CloudFoundryTokenValidator
|
||||
public class CloudFoundryWcfTokenValidator : CloudFoundryTokenValidator
|
||||
{
|
||||
public CloudFoundryOptions Options { get; internal protected set; }
|
||||
|
||||
private static ILogger<CloudFoundryTokenValidator> _logger;
|
||||
private JwtSecurityTokenHandler _handler = new JwtSecurityTokenHandler();
|
||||
|
||||
public CloudFoundryTokenValidator(CloudFoundryOptions options)
|
||||
public CloudFoundryWcfTokenValidator(CloudFoundryOptions options, ILogger<CloudFoundryTokenValidator> logger = null)
|
||||
: base(options)
|
||||
{
|
||||
Options = options ?? throw new ArgumentNullException("options null");
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static void ThrowJwtException(string exeptionMessage, string message)
|
||||
/// <summary>
|
||||
/// <para>Throws a WebFaultException with HTTP 401. exceptionMessage used for Detail if provided, else uses message</para>
|
||||
/// Sets WWW-Authenticate value = 'Bearer realm="default",error="{message}",error_description="{exceptionMessage}'"
|
||||
/// </summary>
|
||||
/// <param name="exceptionMessage">Falls back to value of message if null or empty</param>
|
||||
/// <param name="message">Uses value "invalid_token" if not provided</param>
|
||||
public static void ThrowJwtException(string exceptionMessage, string message)
|
||||
{
|
||||
Console.Out.WriteLine("error: " + exeptionMessage + " " + message);
|
||||
_logger?.LogError("Encountered JWT-related exception: " + exceptionMessage + " " + message);
|
||||
if (WebOperationContext.Current != null)
|
||||
{
|
||||
var headers = WebOperationContext.Current.OutgoingResponse.Headers;
|
||||
|
||||
// https://tools.ietf.org/html/rfc6750 - "WWW-Authenticate", "Bearer error=\"insufficient_scope\"");
|
||||
// https://tools.ietf.org/html/rfc6750#section-3 - "WWW-Authenticate", "Bearer error=\"insufficient_scope\"");
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
message = "invalid_token";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(exeptionMessage))
|
||||
if (string.IsNullOrEmpty(exceptionMessage))
|
||||
{
|
||||
exeptionMessage = message;
|
||||
exceptionMessage = message;
|
||||
}
|
||||
|
||||
headers.Add(HttpResponseHeader.WwwAuthenticate, string.Format("Bearer realm=\"default\",error=\"{0}\",error_description=\"{1}\"", message, Regex.Replace(exeptionMessage, @"\s+", " ")));
|
||||
headers.Add(HttpResponseHeader.WwwAuthenticate, string.Format("Bearer realm=\"default\",error=\"{0}\",error_description=\"{1}\"", message, Regex.Replace(exceptionMessage, @"\s+", " ")));
|
||||
}
|
||||
|
||||
throw new WebFaultException<string>(exeptionMessage ?? message, HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
public virtual string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
|
||||
{
|
||||
if (issuer.Contains("uaa"))
|
||||
WebOperationContext ctx = WebOperationContext.Current;
|
||||
if (ctx != null)
|
||||
{
|
||||
return issuer;
|
||||
ctx.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual bool ValidateAudience(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
|
||||
{
|
||||
foreach (string audience in audiences)
|
||||
{
|
||||
if (audience.Equals(Options.ClientId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Options.AdditionalAudiences != null)
|
||||
{
|
||||
bool found = Options.AdditionalAudiences.Any(x => x.Equals(audience));
|
||||
if (found)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
throw new WebFaultException<string>(exceptionMessage ?? message, HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
public virtual ClaimsPrincipal ValidateToken(string token)
|
||||
|
@ -109,7 +91,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("ValidateToken fails:" + ex.Message);
|
||||
_logger?.LogError("ValidateToken failed: " + ex.Message);
|
||||
ThrowJwtException(ex.Message, "invalid_token");
|
||||
}
|
||||
|
||||
|
@ -128,36 +110,5 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
|
||||
return principal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method validate scopes provided in configuration,
|
||||
/// to perform scope based Authorization
|
||||
/// </summary>
|
||||
/// <param name="validJwt">JSON Web token</param>
|
||||
/// <returns>true if scopes validated</returns>
|
||||
protected virtual bool ValidateScopes(JwtSecurityToken validJwt)
|
||||
{
|
||||
if (Options.RequiredScopes == null || Options.RequiredScopes.Count<string>() == 0)
|
||||
{
|
||||
return true; // nocheck
|
||||
}
|
||||
|
||||
if (!validJwt.Claims.Any(x => x.Type.Equals("scope") || x.Type.Equals("authorities")))
|
||||
{
|
||||
return false; // no scopes at all
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
foreach (Claim claim in validJwt.Claims)
|
||||
{
|
||||
if (claim.Type.Equals("scope") || (claim.Type.Equals("authorities")
|
||||
&& Options.RequiredScopes.Any(x => x.Equals(claim.Value))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,9 +13,11 @@
|
|||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
[Obsolete]
|
||||
public class JsonWebKeySetEx : JsonWebKeySet
|
||||
{
|
||||
public JsonWebKeySetEx(string json)
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.ServiceModel;
|
||||
using System.ServiceModel.Channels;
|
||||
|
@ -36,6 +38,43 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
_options = options;
|
||||
}
|
||||
|
||||
internal ClaimsPrincipal GetPrincipalFromRequestHeaders(WebHeaderCollection headers)
|
||||
{
|
||||
// Fail if SSO Config is missing
|
||||
if (_options?.AuthorizationUrl == null || _options?.AuthorizationUrl?.Length == 0)
|
||||
{
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("SSO Configuration is missing", null);
|
||||
}
|
||||
|
||||
// check if any auth header is present
|
||||
if (string.IsNullOrEmpty(headers["Authorization"]))
|
||||
{
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("No Authorization header", null);
|
||||
}
|
||||
|
||||
// check if the auth header has a bearer token format
|
||||
if (!headers["Authorization"].StartsWith("Bearer", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("Wrong Token Format", null);
|
||||
}
|
||||
|
||||
// get just the token out of the header value
|
||||
string jwt;
|
||||
try
|
||||
{
|
||||
jwt = headers["Authorization"].Split(' ')[1];
|
||||
|
||||
// Return an identity from validated token
|
||||
return _options.TokenValidator.ValidateToken(jwt);
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("No Token", null);
|
||||
}
|
||||
|
||||
throw new Exception("idk");
|
||||
}
|
||||
|
||||
protected override bool CheckAccessCore(OperationContext operationContext)
|
||||
{
|
||||
HttpRequestMessageProperty httpRequestMessage;
|
||||
|
@ -43,40 +82,13 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
if (operationContext.RequestContext.RequestMessage.Properties.TryGetValue(HttpRequestMessageProperty.Name, out object httpRequestMessageObject))
|
||||
{
|
||||
httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
|
||||
if (string.IsNullOrEmpty(httpRequestMessage.Headers["Authorization"]))
|
||||
var claimsPrincipal = GetPrincipalFromRequestHeaders(httpRequestMessage.Headers);
|
||||
if (claimsPrincipal != null)
|
||||
{
|
||||
CloudFoundryTokenValidator.ThrowJwtException("No Authorization header", null);
|
||||
// Set the Principal created from token
|
||||
SetPrincipal(operationContext, claimsPrincipal);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get Bearer token
|
||||
if (!httpRequestMessage.Headers["Authorization"].StartsWith("Bearer "))
|
||||
{
|
||||
CloudFoundryTokenValidator.ThrowJwtException("No Token", null);
|
||||
}
|
||||
|
||||
string jwt = httpRequestMessage.Headers["Authorization"].Split(' ')[1];
|
||||
if (string.IsNullOrEmpty(jwt))
|
||||
{
|
||||
CloudFoundryTokenValidator.ThrowJwtException("Wrong Token Format", null);
|
||||
}
|
||||
|
||||
// Get SSO Config
|
||||
if (_options?.OAuthServiceUrl == null || _options?.OAuthServiceUrl?.Length == 0)
|
||||
{
|
||||
CloudFoundryTokenValidator.ThrowJwtException("SSO Configuration is missing", null);
|
||||
}
|
||||
|
||||
// Validate Token
|
||||
ClaimsPrincipal claimsPrincipal = _options.TokenValidator.ValidateToken(jwt);
|
||||
if (claimsPrincipal == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set the Principal created from token
|
||||
SetPrincipal(operationContext, claimsPrincipal);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -14,8 +14,10 @@
|
|||
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.ServiceModel.Channels;
|
||||
using System.ServiceModel.Configuration;
|
||||
using System.ServiceModel.Description;
|
||||
using System.ServiceModel.Dispatcher;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
{
|
||||
|
@ -50,16 +52,16 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
get { return typeof(JwtHeaderEndpointBehavior); }
|
||||
}
|
||||
|
||||
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
|
||||
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
|
||||
{
|
||||
clientRuntime.ClientMessageInspectors.Add(new JwtHeaderMessageInspector(_options, _userToken));
|
||||
}
|
||||
|
||||
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
|
||||
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
|
||||
{
|
||||
}
|
||||
|
||||
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
|
||||
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -70,7 +72,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
protected override object CreateBehavior()
|
||||
{
|
||||
// Create the endpoint behavior that will insert the message inspector into the client runtime
|
||||
return new JwtHeaderEndpointBehavior(_options);
|
||||
return new JwtHeaderEndpointBehavior(_options, _userToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.ServiceModel;
|
||||
using System.ServiceModel.Channels;
|
||||
using System.ServiceModel.Dispatcher;
|
||||
|
@ -22,14 +23,16 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
{
|
||||
public class JwtHeaderMessageInspector : IClientMessageInspector
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly CloudFoundryOptions _options;
|
||||
private string _token;
|
||||
private CloudFoundryOptions _options;
|
||||
private string _userToken;
|
||||
|
||||
public JwtHeaderMessageInspector(CloudFoundryOptions options, string userToken)
|
||||
public JwtHeaderMessageInspector(CloudFoundryOptions options, string userToken, HttpClient httpClient = null)
|
||||
{
|
||||
_options = options;
|
||||
_userToken = userToken;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public object BeforeSendRequest(ref Message request, IClientChannel channel)
|
||||
|
@ -78,7 +81,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
|
||||
private string GetAccessToken()
|
||||
{
|
||||
CloudFoundryClientTokenResolver tokenResolver = new CloudFoundryClientTokenResolver(_options);
|
||||
CloudFoundryClientTokenResolver tokenResolver = new CloudFoundryClientTokenResolver(_options, _httpClient);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
string matchACL = Environment.GetEnvironmentVariable(Role);
|
||||
if (string.IsNullOrEmpty(matchACL))
|
||||
{
|
||||
CloudFoundryTokenValidator.ThrowJwtException("Configuration for not provided for Role: " + Role, "insufficient_scope");
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("Configuration for not provided for Role: " + Role, "insufficient_scope");
|
||||
}
|
||||
|
||||
IPrincipal principal = Thread.CurrentPrincipal;
|
||||
|
@ -58,7 +58,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
else
|
||||
{
|
||||
Console.Out.WriteLine("Access denied user is not in Role: " + Role);
|
||||
CloudFoundryTokenValidator.ThrowJwtException("Access denied user is not in Role: " + Role, "insufficient_scope");
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("Access denied user is not in Role: " + Role, "insufficient_scope");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Steeltoe.Security.Authentication.CloudFoundryWcf.Test")]
|
|
@ -16,7 +16,6 @@ using System;
|
|||
using System.Security;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Permissions;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
||||
|
@ -43,7 +42,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf
|
|||
if (principal == null || !principal.HasClaim("scope", Scope))
|
||||
{
|
||||
Console.Out.WriteLine("Access denied token is not in Scope: " + Scope);
|
||||
CloudFoundryTokenValidator.ThrowJwtException("Access denied token does not have Scope: " + Scope, "insufficient_scope");
|
||||
CloudFoundryWcfTokenValidator.ThrowJwtException("Access denied token does not have Scope: " + Scope, "insufficient_scope");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Steeltoe.CloudFoundry.ConnectorBase" Version="$(SteeltoeConnectorVersion)" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="$(JwtTokensVersion)" />
|
||||
<PackageReference Include="System.Net.Http" Version="$(HttpClientVersion)" />
|
||||
<PackageReference Include="Steeltoe.Common.Http" Version="$(SteeltoeCommonVersion)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
|
||||
|
@ -41,6 +40,7 @@
|
|||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
<RootNamespace>Steeltoe.Security.Authentication.CloudFoundry</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
|
@ -48,5 +48,10 @@
|
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == ''">
|
||||
<ProjectReference Include="..\Steeltoe.Security.Authentication.CloudFoundryBase\Steeltoe.Security.Authentication.CloudFoundryBase.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(CI_BUILD)' == 'True'">
|
||||
<PackageReference Include="Steeltoe.Security.Authentication.CloudFoundryBase" Version="$(SteeltoeVersion)$(SteeltoeVersionSuffix)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -38,6 +38,19 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
Assert.Null(result2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTokenValidationParameters_ReturnsExpected()
|
||||
{
|
||||
var parameters = CloudFoundryHelper.GetTokenValidationParameters(null, "http://foo.bar.com/keyurl", null, false);
|
||||
Assert.False(parameters.ValidateAudience);
|
||||
Assert.True(parameters.ValidateIssuer);
|
||||
Assert.NotNull(parameters.IssuerValidator);
|
||||
|
||||
// Assert.Equal(cftv.ValidateIssuer, parameters.IssuerValidator);
|
||||
Assert.True(parameters.ValidateLifetime);
|
||||
Assert.NotNull(parameters.IssuerSigningKeyResolver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetExpTime_FindsTime()
|
||||
{
|
|
@ -21,8 +21,16 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
[Fact]
|
||||
public void ValidateIssuer_ValidatesCorrectly()
|
||||
{
|
||||
Assert.NotNull(CloudFoundryTokenValidator.ValidateIssuer("https://uaa.system.testcloud.com/", null, null));
|
||||
Assert.Null(CloudFoundryTokenValidator.ValidateIssuer("https://foobar.system.testcloud.com/", null, null));
|
||||
// arrange
|
||||
var cftv = new CloudFoundryTokenValidator();
|
||||
|
||||
// act
|
||||
var uaaResult = cftv.ValidateIssuer("https://uaa.system.testcloud.com/", null, null);
|
||||
var foobarResult = cftv.ValidateIssuer("https://foobar.system.testcloud.com/", null, null);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(uaaResult);
|
||||
Assert.Null(foobarResult);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net461</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Steeltoe.Security.Authentication.CloudFoundryBase\Steeltoe.Security.Authentication.CloudFoundryBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="$(MockHttp)" />
|
||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitStudioVersion)" />
|
||||
<DotNetCliToolReference Include="dotnet-xunit" Version="$(XunitVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreTestVersion)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
<RootNamespace>Steeltoe.Security.Authentication.CloudFoundry.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
<Link>stylecop.json</Link>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -18,32 +18,6 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
{
|
||||
public class TestHelpers
|
||||
{
|
||||
public static string CreateTempFile(string contents)
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
File.WriteAllText(tempFile, contents);
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
public static Stream StringToStream(string str)
|
||||
{
|
||||
var memStream = new MemoryStream();
|
||||
var textWriter = new StreamWriter(memStream);
|
||||
textWriter.Write(str);
|
||||
textWriter.Flush();
|
||||
memStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
return memStream;
|
||||
}
|
||||
|
||||
public static string StreamToString(Stream stream)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var reader = new StreamReader(stream);
|
||||
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
public static string GetValidTokenRequestResponse()
|
||||
{
|
||||
return @"{'access_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJjNjUxZGI2NjllODM0NGNkOTMwMGUxYjJkYjUxOTgwZCIsInN1YiI6IjEzYmI2ODQxLWU0ZDYtNGE5YS04NzZjLTllZjEzYWE2MWNjNyIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJjaWQiOiI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJhenAiOiI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6IjEzYmI2ODQxLWU0ZDYtNGE5YS04NzZjLTllZjEzYWE2MWNjNyIsIm9yaWdpbiI6InVhYSIsInVzZXJfbmFtZSI6InRlc3Rzc291c2VyIiwiZW1haWwiOiJ0ZXN0c3NvdXNlckB0ZXN0Y2xvdWQuY29tIiwiYXV0aF90aW1lIjoxNDcyNzUxNDUyLCJyZXZfc2lnIjoiNTExMzU4NDIiLCJpYXQiOjE0NzI3NTE0NTIsImV4cCI6MTQ3Mjc5NDY1MiwiaXNzIjoiaHR0cHM6Ly90ZXN0c3NvLnVhYS5zeXN0ZW0udGVzdGNsb3VkLmNvbS9vYXV0aC90b2tlbiIsInppZCI6ImVmMzA0ZWQwLTNhNWEtNDQyZS05YWQ4LWU0Y2EzYWQ1MTIwZSIsImF1ZCI6WyI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJvcGVuaWQiXX0.f6CA7WrGrY__CNh - RvY1pl05 - 2PQjeyE7C_OH48ChfoxsL4hO8BJEofz934_Jox6dXJwp3wyeMPYwyS5fKGA3ASJT5KqdZ8QdHhOj7lbcejwTUUfG_t - K26W39BLT - cEbpTFyHzBUK79fhIfwSmmXJWU0vQaZ3F0dYKRm98rCEQo9s5jr2jrqHep52vL3Xo8mwDJO4GBc85p8zzVpUPOYMkKiWfEujIOvBL0yTqAL64NsYmFupbKyt5dgf - eF - 1GOuP7Rbgbk_llQuFOkwYr4dLzgw045wXeU3uzJzTrGNzIlJQPOijU1w2uBgYi7fT2wgJDKb4toxot2_6wSXpqBw','token_type':'bearer','refresh_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJjNjUxZGI2NjllODM0NGNkOTMwMGUxYjJkYjUxOTgwZC1yIiwic3ViIjoiMTNiYjY4NDEtZTRkNi00YTlhLTg3NmMtOWVmMTNhYTYxY2M3Iiwic2NvcGUiOlsib3BlbmlkIl0sImlhdCI6MTQ3Mjc1MTQ1MiwiZXhwIjoxNDc1MzQzNDUyLCJjaWQiOiI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJjbGllbnRfaWQiOiI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJpc3MiOiJodHRwczovL3Rlc3Rzc28udWFhLnN5c3RlbS50ZXN0Y2xvdWQuY29tL29hdXRoL3Rva2VuIiwiemlkIjoiZWYzMDRlZDAtM2E1YS00NDJlLTlhZDgtZTRjYTNhZDUxMjBlIiwiZ3JhbnRfdHlwZSI6ImF1dGhvcml6YXRpb25fY29kZSIsInVzZXJfbmFtZSI6InRlc3Rzc291c2VyIiwib3JpZ2luIjoidWFhIiwidXNlcl9pZCI6IjEzYmI2ODQxLWU0ZDYtNGE5YS04NzZjLTllZjEzYWE2MWNjNyIsInJldl9zaWciOiI1MTEzNTg0MiIsImF1ZCI6WyI0NGY4NTRmZC00OWZlLTQxMDItYjBkMS1hNTI2ZGJmZjkzZDkiLCJvcGVuaWQiXX0.WMEZtwvoJXU8njysozaNglmpYouxoY65AirXC3ypdLsnSVE_OtePQQi3hb3YPQDbrc6JAETnCGcDBJVNUPsnlg4g_O_RPLejB4ZlMioRTZRNv9gzOlELqjgBLAVEkWOiBFuH7hwbR7X7DlmFTcDFSc - gQY5TaLn - wR6PVtQM3oRw_nx1f3 - xgDGcYx7Ktk9rov - xF_C0Pzsoet4dvG78YUlnXgR08KwJnDX_ElV - 0vTOtybqjs_XGSS9N39EWeeykCzciddhqVCfmMM5VMCVaP - 7pr - VDz0N8ArSWfOobI4nPqcj50wvPb595SgId - NlNP4fBX1ibnCeOxWwuzId0w','expires_in':43199,'scope':'openid','jti':'c651db669e8344cd9300e1b2db51980d'}";
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using RichardSzalay.MockHttp;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
||||
{
|
||||
public class TokenExchangerTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetTokenRequestMessage_ReturnsCorrectly()
|
||||
{
|
||||
// arrange
|
||||
var opts = new AuthServerOptions { ClientId = "clientId", ClientSecret = "clientSecret" };
|
||||
var tEx = new TokenExchanger(opts);
|
||||
|
||||
// act
|
||||
var message = tEx.GetTokenRequestMessage(new List<KeyValuePair<string, string>>(), "redirectUri");
|
||||
|
||||
// assert
|
||||
Assert.NotNull(message);
|
||||
var content = message.Content as FormUrlEncodedContent;
|
||||
Assert.NotNull(content);
|
||||
Assert.Equal(HttpMethod.Post, message.Method);
|
||||
|
||||
Assert.Contains(new MediaTypeWithQualityHeaderValue("application/json"), message.Headers.Accept);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthCodeTokenRequestParameters_ReturnsCorrectly()
|
||||
{
|
||||
// arrange
|
||||
var opts = new AuthServerOptions { ClientId = "clientId", ClientSecret = "clientSecret", CallbackUrl = "redirect_uri" };
|
||||
var tEx = new TokenExchanger(opts);
|
||||
|
||||
// act
|
||||
var parameters = tEx.AuthCodeTokenRequestParameters("authcode");
|
||||
|
||||
// assert
|
||||
Assert.NotNull(parameters);
|
||||
Assert.Equal(opts.ClientId, parameters.First(i => i.Key == "client_id").Value);
|
||||
Assert.Equal(opts.ClientSecret, parameters.First(i => i.Key == "client_secret").Value);
|
||||
Assert.Equal("redirect_uri", parameters.First(i => i.Key == "redirect_uri").Value);
|
||||
Assert.Equal("authcode", parameters.First(i => i.Key == "code").Value);
|
||||
Assert.Equal(OpenIdConnectGrantTypes.AuthorizationCode, parameters.First(i => i.Key == "grant_type").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClientCredentialsTokenRequestParameters_ReturnsCorrectly()
|
||||
{
|
||||
// arrange
|
||||
var opts = new AuthServerOptions { ClientId = "clientId", ClientSecret = "clientSecret", CallbackUrl = "redirect_uri" };
|
||||
var tEx = new TokenExchanger(opts);
|
||||
|
||||
// act
|
||||
var parameters = tEx.ClientCredentialsTokenRequestParameters();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(parameters);
|
||||
Assert.Equal(opts.ClientId, parameters.First(i => i.Key == "client_id").Value);
|
||||
Assert.Equal(opts.ClientSecret, parameters.First(i => i.Key == "client_secret").Value);
|
||||
Assert.Equal(OpenIdConnectGrantTypes.ClientCredentials, parameters.First(i => i.Key == "grant_type").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ExchangeAuthCodeForClaimsIdentity_ExchangesCodeForIdentity()
|
||||
{
|
||||
// arrange
|
||||
var options = new AuthServerOptions()
|
||||
{
|
||||
AuthorizationUrl = "http://localhost/tokenUrl"
|
||||
};
|
||||
var exchanger = new TokenExchanger(options, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var identity = await exchanger.ExchangeAuthCodeForClaimsIdentity("goodCode");
|
||||
|
||||
// assert
|
||||
Assert.IsType<ClaimsIdentity>(identity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ExchangeAuthCodeForClaimsIdentity_ReturnsNullOnFailure()
|
||||
{
|
||||
// arrange
|
||||
var options = new AuthServerOptions()
|
||||
{
|
||||
AuthorizationUrl = "http://localhost/tokenUrl"
|
||||
};
|
||||
var httpClient = new object(); // TODO: replace with mock that does stuff
|
||||
var exchanger = new TokenExchanger(options, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var identity = await exchanger.ExchangeAuthCodeForClaimsIdentity("badCode");
|
||||
|
||||
// assert
|
||||
Assert.Null(identity);
|
||||
}
|
||||
|
||||
private readonly string realTokenResponse = "{'access_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI3YTMzYzVhNjhjY2I0YjRiYmQ5N2I4MTRlZWExMTc3MiIsInN1YiI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsInNjb3BlIjpbInRlc3Rncm91cCIsIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJhenAiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsIm9yaWdpbiI6InVhYSIsInVzZXJfbmFtZSI6ImRhdmUiLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsImF1dGhfdGltZSI6MTU0NjY0MjkzMywicmV2X3NpZyI6ImE1ZWY2ODg5IiwiaWF0IjoxNTQ2NjQyOTM1LCJleHAiOjE1NDY2ODYxMzUsImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsImF1ZCI6WyJvcGVuaWQiLCJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiXX0.tGTXZzuuUSObTwdPHSx-zvnld20DH5hlOZlYp5DhjwkMIsZB0uIvVwbVDkPp7H_AmmeJoo6vqa5hbbgfgnYpTrKlCGOypnHoa3yRIKrwcDmLLujaMz6ApZeaJ7sJN-0N1UnPZ9iGcqvt9hNb_198zRnMXGH72oI0e2iGUBV1olCFVdZTnMGT7sUieDFKy7n0ghZYq_gUI8rfvTwiC3lfxv0nDXz4oE9Z-UKhK6q1zkAtQrz61FQ_CHONejz1JnuxQFKMMvm8JLcRkn6OL-EcSi1hkmFw0efO1OqccQacxphlafyHloVPQ3IOtzLjCf8sJ5NgTdCTC3iddT_sYovdrg','token_type':'bearer','id_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJzdWIiOiI5NWJiYjM0Ni1iNjhjLTRmMTUtYjM0MS03MGQ2MGY5ZjQ2YmEiLCJhdWQiOlsiYzkyMGI0ZjUtNDg3Yy00ZGQwLWE2M2QtNGQ0MGMxMzMxOTg2Il0sImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsImV4cCI6MTU0NjY4NjEzNSwiaWF0IjoxNTQ2NjQyOTM1LCJhbXIiOlsicHdkIl0sImF6cCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsIm9yaWdpbiI6InVhYSIsImp0aSI6IjdhMzNjNWE2OGNjYjRiNGJiZDk3YjgxNGVlYTExNzcyIiwicHJldmlvdXNfbG9nb25fdGltZSI6MTU0NjYzMzU1NjA0NCwiZW1haWxfdmVyaWZpZWQiOnRydWUsImNsaWVudF9pZCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsImNpZCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJ1c2VyX25hbWUiOiJkYXZlIiwicmV2X3NpZyI6ImE1ZWY2ODg5IiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsImF1dGhfdGltZSI6MTU0NjY0MjkzM30.KkTVOTg7Bhj1EWO63QmWzjnEAnKesoSLGfGL-2Y19PiK62KRd66dOcVcQEA_nWIE1mJQZsDByQYwcEuVRAiP-mXY0L2MrWUnRlW5yn1fqOc44iSDggMF5VfjGQok8fGfBPQX7va0evfaOaulRMuWsijYvzZtV-KncGUpxGwkzRs2AAEbkAv1_vAD2zSGJ-ji5L7s4a2-Qc_LxDlNANoYllzMTVxZ2DSvVPLfKPNGgSvNC7t053ExfGRXk-6cPxVznkngWDlYALeXsnrbvXdjuk1dw8dcXRhL4PUJDI7EVvTdqzd1fPYRgAQ3KJZOmvzBY7bxFtoq9odmKKHTI4CFUQ','refresh_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI4NjdiNTcxNTBlNmM0ZDQ3OTM0NmE3ZjgwMTJmMGY2My1yIiwic3ViIjoiOTViYmIzNDYtYjY4Yy00ZjE1LWIzNDEtNzBkNjBmOWY0NmJhIiwic2NvcGUiOlsidGVzdGdyb3VwIiwib3BlbmlkIl0sImlhdCI6MTU0NjY0MjkzNSwiZXhwIjoxNTQ5MjM0OTM1LCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJpc3MiOiJodHRwczovL3N0ZWVsdG9lLnVhYS5jZi5iZWV0LnNwcmluZ2FwcHMuaW8vb2F1dGgvdG9rZW4iLCJ6aWQiOiIzYTNlYWRhZC01YjJmLTQ1MzAtYWY5NS1hNjliYzBhZmQxNWIiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9uYW1lIjoiZGF2ZSIsIm9yaWdpbiI6InVhYSIsInVzZXJfaWQiOiI5NWJiYjM0Ni1iNjhjLTRmMTUtYjM0MS03MGQ2MGY5ZjQ2YmEiLCJyZXZfc2lnIjoiYTVlZjY4ODkiLCJhdWQiOlsib3BlbmlkIiwiYzkyMGI0ZjUtNDg3Yy00ZGQwLWE2M2QtNGQ0MGMxMzMxOTg2Il19.cOHCiZgmbtN1uyuvEou879du5XVvJqHEmkf6-2G0V9I1qYy0PJbGMHNL32d6L9LVsnXs4iTfi1qpKfdEGbfZbX8rNwNwcRoPndOZBepTTZHJPcU49VEF4t1hZ5icbt_4mSiQwpNy2n_mqwHizV8BAeOeQc_zUE-YFiuQC6V3ac0rTYO4NJGSioSG_y6HFp3refiPZVPT7En-dwhd-Yic1SB1OPxQ5bRNS7AAjGLeeCWOZQVxwQa5Atv9t791yUkH1zX8Psh_LRy2O8E6o7IBLI_yjz5N-YIHRa-BuMPDdKWarcOjy1KKJM27HynAQo1T4J0Ft8hSsIxFlObTd3-FVQ','expires_in':43199,'scope':'testgroup openid','jti':'7a33c5a68ccb4b4bbd97b814eea11772'}";
|
||||
|
||||
private HttpClient GetMockHttpClient()
|
||||
{
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
|
||||
mockHttp
|
||||
.When("http://localhost/tokenUrl")
|
||||
.WithFormData("code", "goodCode")
|
||||
.Respond("application/json", realTokenResponse); // Respond with JSON
|
||||
mockHttp
|
||||
.When("http://localhost/tokenUrl")
|
||||
.WithFormData("code", "badCode")
|
||||
.Respond(HttpStatusCode.BadRequest); // Respond with JSON
|
||||
|
||||
return mockHttp.ToHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,24 +43,12 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
JwtBearerOptions jwtOpts = new JwtBearerOptions();
|
||||
|
||||
CloudFoundryJwtBearerConfigurer.Configure(info, jwtOpts, opts);
|
||||
Assert.Equal("http://domain" + CloudFoundryDefaults.JwtTokenKey, opts.JwtKeyUrl);
|
||||
Assert.Equal("http://domain" + CloudFoundryDefaults.JwtTokenUri, opts.JwtKeyUrl);
|
||||
Assert.True(opts.ValidateCertificates);
|
||||
Assert.Equal(opts.ClaimsIssuer, jwtOpts.ClaimsIssuer);
|
||||
Assert.Null(jwtOpts.BackchannelHttpHandler);
|
||||
Assert.NotNull(jwtOpts.TokenValidationParameters);
|
||||
Assert.Equal(opts.SaveToken, jwtOpts.SaveToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTokenValidationParameters_ReturnsExpected()
|
||||
{
|
||||
var parameters = CloudFoundryJwtBearerConfigurer.GetTokenValidationParameters(null, "http://foo.bar.com/keyurl", null, false);
|
||||
Assert.False(parameters.ValidateAudience);
|
||||
Assert.True(parameters.ValidateIssuer);
|
||||
Assert.NotNull(parameters.IssuerValidator);
|
||||
Assert.Equal(CloudFoundryTokenValidator.ValidateIssuer, parameters.IssuerValidator);
|
||||
Assert.True(parameters.ValidateLifetime);
|
||||
Assert.NotNull(parameters.IssuerSigningKeyResolver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
|
||||
string authURL = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
Assert.Equal(CloudFoundryDefaults.AuthenticationScheme, opts.ClaimsIssuer);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.JwtTokenKey, opts.JwtKeyUrl);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.JwtTokenUri, opts.JwtKeyUrl);
|
||||
Assert.True(opts.SaveToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,45 +33,6 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
{
|
||||
public class CloudFoundryOAuthHandlerTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetTokenRequestParameters_ReturnsCorrectly()
|
||||
{
|
||||
var opts = new CloudFoundryOAuthOptions()
|
||||
{
|
||||
Backchannel = new HttpClient(new TestMessageHandler())
|
||||
};
|
||||
|
||||
MyTestCloudFoundryHandler testHandler = GetTestHandler(opts);
|
||||
|
||||
var parameters = testHandler.GetTokenRequestParameters("code", "redirectUri");
|
||||
Assert.NotNull(parameters);
|
||||
|
||||
Assert.Equal(parameters["client_id"], opts.ClientId);
|
||||
Assert.Equal("redirectUri", parameters["redirect_uri"]);
|
||||
Assert.Equal(parameters["client_secret"], opts.ClientSecret);
|
||||
Assert.Equal("code", parameters["code"]);
|
||||
Assert.Equal("authorization_code", parameters["grant_type"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTokenRequestMessage_ReturnsCorrectly()
|
||||
{
|
||||
var opts = new CloudFoundryOAuthOptions()
|
||||
{
|
||||
Backchannel = new HttpClient(new TestMessageHandler())
|
||||
};
|
||||
|
||||
MyTestCloudFoundryHandler testHandler = GetTestHandler(opts);
|
||||
|
||||
var message = testHandler.GetTokenRequestMessage("code", "redirectUri");
|
||||
Assert.NotNull(message);
|
||||
var content = message.Content as FormUrlEncodedContent;
|
||||
Assert.NotNull(content);
|
||||
Assert.Equal(HttpMethod.Post, message.Method);
|
||||
|
||||
message.Headers.Accept.Contains(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ExchangeCodeAsync_SendsTokenRequest_ReturnsValidTokenInfo()
|
||||
{
|
||||
|
@ -121,7 +82,7 @@ namespace Steeltoe.Security.Authentication.CloudFoundry.Test
|
|||
|
||||
MyTestCloudFoundryHandler testHandler = GetTestHandler(opts);
|
||||
var logger = new LoggerFactory().CreateLogger("ExchangeCodeAsync_SendsTokenRequest");
|
||||
var resp = await testHandler.TestExchangeCodeAsync("code", "redirectUri");
|
||||
var resp = await testHandler.TestExchangeCodeAsync("code", "http://redirectUri");
|
||||
|
||||
Assert.NotNull(handler.LastRequest);
|
||||
Assert.Equal(HttpMethod.Post, handler.LastRequest.Method);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -11,6 +11,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Steeltoe.Security.Authentication.CloudFoundryCore\Steeltoe.Security.Authentication.CloudFoundryCore.csproj" />
|
||||
<ProjectReference Include="..\Steeltoe.Security.Authentication.CloudFoundryBaseTest\Steeltoe.Security.Authentication.CloudFoundryBase.Test.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Owin.Builder;
|
||||
using Owin;
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin.Test
|
||||
{
|
||||
public class IAppBuilderExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void UseCloudFoundryOpenIdConnect_ThrowsIfBuilderNull()
|
||||
{
|
||||
IAppBuilder app = null;
|
||||
IConfiguration config = new ConfigurationBuilder().Build();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => app.UseCloudFoundryOpenIdConnect(config));
|
||||
Assert.Equal("appBuilder", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseCloudFoundryOpenIdConnect_ThrowsIfConfigurationNull()
|
||||
{
|
||||
IAppBuilder app = new AppBuilder();
|
||||
IConfiguration config = null;
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => app.UseCloudFoundryOpenIdConnect(config));
|
||||
Assert.Equal("configuration", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseCloudFoundryJwtBearerAuthentication_ThrowsIfBuilderNull()
|
||||
{
|
||||
IAppBuilder app = null;
|
||||
IConfiguration config = new ConfigurationBuilder().Build();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => app.UseCloudFoundryJwtBearerAuthentication(config));
|
||||
Assert.Equal("appBuilder", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseCloudFoundryJwtBearerAuthentication_ThrowsIfConfigurationNull()
|
||||
{
|
||||
IAppBuilder app = new AppBuilder();
|
||||
IConfiguration config = null;
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => app.UseCloudFoundryJwtBearerAuthentication(config));
|
||||
Assert.Equal("configuration", exception.ParamName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin.Test
|
||||
{
|
||||
public class CloudFoundryJwtOwinConfigurerTest
|
||||
{
|
||||
[Fact]
|
||||
public void Configure_NoServiceInfo_ReturnsExpected()
|
||||
{
|
||||
// arrange
|
||||
CloudFoundryJwtBearerAuthenticationOptions opts = new CloudFoundryJwtBearerAuthenticationOptions();
|
||||
|
||||
// act
|
||||
CloudFoundryJwtOwinConfigurer.Configure(null, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal("http://" + CloudFoundryDefaults.OAuthServiceUrl + CloudFoundryDefaults.JwtTokenUri, opts.JwtKeyUrl);
|
||||
Assert.True(opts.ValidateCertificates); // <- default value
|
||||
Assert.NotNull(opts.TokenValidationParameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_NoOptions_ReturnsExpected()
|
||||
{
|
||||
// arrange
|
||||
SsoServiceInfo info = new SsoServiceInfo("foobar", "clientId", "secret", "http://domain");
|
||||
|
||||
// act
|
||||
CloudFoundryJwtOwinConfigurer.Configure(info, null);
|
||||
|
||||
// nothing to assert
|
||||
Assert.True(true, "If we got here, we didn't attempt to set properties on a null object");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_WithServiceInfo_ReturnsExpected()
|
||||
{
|
||||
// arrange
|
||||
CloudFoundryJwtBearerAuthenticationOptions opts = new CloudFoundryJwtBearerAuthenticationOptions();
|
||||
Assert.Null(opts.TokenValidationParameters);
|
||||
SsoServiceInfo info = new SsoServiceInfo("foobar", "clientId", "secret", "http://domain");
|
||||
|
||||
// act
|
||||
CloudFoundryJwtOwinConfigurer.Configure(info, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal("http://domain" + CloudFoundryDefaults.JwtTokenUri, opts.JwtKeyUrl);
|
||||
Assert.True(opts.ValidateCertificates); // <- default value
|
||||
Assert.NotNull(opts.TokenValidationParameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Owin;
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin.Test
|
||||
{
|
||||
public class OpenIdConnectConfigurerTest
|
||||
{
|
||||
[Fact]
|
||||
public void Configure_NoOptions_Throws()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => OpenIdConnectConfigurer.Configure(null, null));
|
||||
Assert.Equal("options", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_NoServiceInfo_ReturnsDefaults()
|
||||
{
|
||||
// arrange
|
||||
var opts = new OpenIdConnectOptions();
|
||||
string authURL = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
|
||||
// act
|
||||
OpenIdConnectConfigurer.Configure(null, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal(CloudFoundryDefaults.DisplayName, opts.AuthenticationType);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientId, opts.ClientId);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientSecret, opts.ClientSecret);
|
||||
Assert.Equal(new PathString(CloudFoundryDefaults.CallbackPath), opts.CallbackPath);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.CheckTokenUri, opts.TokenInfoUrl);
|
||||
Assert.True(opts.ValidateCertificates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_ObsoleteVersion_NoServiceInfo_ReturnsDefaults()
|
||||
{
|
||||
// arrange
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var opts = new OpenIDConnectOptions();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
string authURL = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
|
||||
// act
|
||||
OpenIdConnectConfigurer.Configure(null, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal("PivotalSSO", opts.AuthenticationType);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientId, opts.ClientId);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientSecret, opts.ClientSecret);
|
||||
Assert.Equal(new PathString("/signin-oidc"), opts.CallbackPath);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.CheckTokenUri, opts.TokenInfoUrl);
|
||||
Assert.True(opts.ValidateCertificates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_WithServiceInfo_ReturnsExpected()
|
||||
{
|
||||
// arrange
|
||||
string authURL = "http://domain";
|
||||
var opts = new OpenIdConnectOptions();
|
||||
SsoServiceInfo info = new SsoServiceInfo("foobar", "clientId", "secret", "http://domain");
|
||||
|
||||
// act
|
||||
OpenIdConnectConfigurer.Configure(info, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal(CloudFoundryDefaults.DisplayName, opts.AuthenticationType);
|
||||
Assert.Equal("clientId", opts.ClientId);
|
||||
Assert.Equal("secret", opts.ClientSecret);
|
||||
Assert.Equal(new PathString(CloudFoundryDefaults.CallbackPath), opts.CallbackPath);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.CheckTokenUri, opts.TokenInfoUrl);
|
||||
Assert.True(opts.ValidateCertificates);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Owin.Security;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin.Test
|
||||
{
|
||||
public class OpenIdConnectOptionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void Options_Use_CloudFoundryDefaults()
|
||||
{
|
||||
// act
|
||||
var options = new OpenIdConnectOptions();
|
||||
|
||||
// assert
|
||||
Assert.Equal(CloudFoundryDefaults.DisplayName, options.AuthenticationType);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientId, options.ClientId);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientSecret, options.ClientSecret);
|
||||
Assert.Equal("http://" + CloudFoundryDefaults.OAuthServiceUrl, options.AuthDomain);
|
||||
Assert.Equal("http://" + CloudFoundryDefaults.OAuthServiceUrl + CloudFoundryDefaults.CheckTokenUri, options.TokenInfoUrl);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientId, options.ClientId);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientId, options.ClientId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Options_Can_Change_AuthenticationType()
|
||||
{
|
||||
// arrange
|
||||
var authType = "NotTheDefault";
|
||||
|
||||
// act
|
||||
var options = new OpenIdConnectOptions(authType);
|
||||
|
||||
// assert
|
||||
Assert.Equal(authType, options.AuthenticationType);
|
||||
Assert.Equal(authType, options.Description.Caption);
|
||||
Assert.Equal(AuthenticationMode.Passive, options.AuthenticationMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenIdOptions_CanProduce_AuthServerOptions()
|
||||
{
|
||||
// arrange
|
||||
var options = new OpenIdConnectOptions
|
||||
{
|
||||
ClientId = "notDefault",
|
||||
ClientSecret = "notDefault",
|
||||
ValidateCertificates = false,
|
||||
AdditionalScopes = "banana apple orange"
|
||||
};
|
||||
|
||||
// act
|
||||
var produce = options.AsAuthServerOptions();
|
||||
|
||||
// assert
|
||||
Assert.Equal(produce.AuthorizationUrl, options.AuthDomain + CloudFoundryDefaults.AccessTokenUri);
|
||||
Assert.Equal(produce.ClientId, options.ClientId);
|
||||
Assert.Equal(produce.ClientSecret, options.ClientSecret);
|
||||
Assert.Equal(produce.ValidateCertificates, options.ValidateCertificates);
|
||||
Assert.Equal(produce.AdditionalTokenScopes, options.AdditionalScopes);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Owin;
|
||||
using Moq;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin.Test
|
||||
{
|
||||
public static class OwinTestHelpers
|
||||
{
|
||||
public static readonly Dictionary<string, string> Appsettings = new Dictionary<string, string>()
|
||||
{
|
||||
};
|
||||
|
||||
public static IOwinContext CreateRequest(string method, string path, string scheme = "http", string host = "localhost", int? port = null, Stream bodyStream = null)
|
||||
{
|
||||
var context = new Mock<OwinContext>();
|
||||
bodyStream = bodyStream ?? new MemoryStream();
|
||||
context.Setup(r => r.Response).Returns(new OwinResponse { Body = bodyStream });
|
||||
context.Setup(r => r.Request).Returns(new OwinRequest
|
||||
{
|
||||
Method = method,
|
||||
Path = new PathString(path),
|
||||
Scheme = scheme,
|
||||
Host = new HostString(host + AddPortIfNotNull(port)),
|
||||
});
|
||||
return context.Object;
|
||||
}
|
||||
|
||||
private static string AddPortIfNotNull(int? port)
|
||||
{
|
||||
if (port != null)
|
||||
{
|
||||
return ":" + port;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreTestVersion)" />
|
||||
<PackageReference Include="Microsoft.Owin.Testing" Version="$(OwinOAuthVersion)" />
|
||||
<PackageReference Include="Moq" Version="$(MoqVersion)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitStudioVersion)" />
|
||||
<DotNetCliToolReference Include="dotnet-xunit" Version="$(XunitVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
<RootNamespace>Steeltoe.Security.Authentication.CloudFoundry.Owin.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
<Link>stylecop.json</Link>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Steeltoe.Security.Authentication.CloudFoundryOwin\Steeltoe.Security.Authentication.CloudFoundryOwin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.Owin;
|
||||
using System.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Owin.Test
|
||||
{
|
||||
public class UriUtilityTest
|
||||
{
|
||||
[Fact]
|
||||
public void OriginalCallbackUri_Not_Changed()
|
||||
{
|
||||
// arrange
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var options = new OpenIDConnectOptions();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
var requestContext = OwinTestHelpers.CreateRequest("GET", string.Empty);
|
||||
|
||||
// act
|
||||
var redirectUri = UriUtility.CalculateFullRedirectUri(options, requestContext.Request);
|
||||
|
||||
// assert
|
||||
Assert.StartsWith(options.AuthDomain, redirectUri);
|
||||
Assert.Contains("response_type=code", redirectUri);
|
||||
Assert.Contains("scope=openid", redirectUri);
|
||||
Assert.EndsWith("redirect_uri=" + WebUtility.UrlEncode("http://localhost/signin-oidc"), redirectUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Can_CalculateFullRedirectUri_WithDefaults()
|
||||
{
|
||||
// arrange
|
||||
var options = new OpenIdConnectOptions();
|
||||
var requestContext = OwinTestHelpers.CreateRequest("GET", string.Empty);
|
||||
|
||||
// act
|
||||
var redirectUri = UriUtility.CalculateFullRedirectUri(options, requestContext.Request);
|
||||
|
||||
// assert
|
||||
Assert.StartsWith(options.AuthDomain, redirectUri);
|
||||
Assert.Contains("response_type=code", redirectUri);
|
||||
Assert.Contains("scope=openid", redirectUri);
|
||||
Assert.EndsWith("redirect_uri=" + WebUtility.UrlEncode("http://localhost" + CloudFoundryDefaults.CallbackPath), redirectUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Can_CalculateFullRedirectUri_WithNonDefaults()
|
||||
{
|
||||
// arrange
|
||||
var options = new OpenIdConnectOptions
|
||||
{
|
||||
AuthDomain = "my_oauth_server",
|
||||
CallbackPath = new PathString("/something_else")
|
||||
};
|
||||
var requestContext = OwinTestHelpers.CreateRequest("GET", string.Empty, "https", "some_server", 1234);
|
||||
|
||||
// act
|
||||
var redirectUri = UriUtility.CalculateFullRedirectUri(options, requestContext.Request);
|
||||
|
||||
// assert
|
||||
Assert.StartsWith(options.AuthDomain, redirectUri);
|
||||
Assert.Contains("response_type=code", redirectUri);
|
||||
Assert.Contains("scope=openid", redirectUri);
|
||||
Assert.EndsWith("redirect_uri=" + WebUtility.UrlEncode("https://some_server:1234/something_else"), redirectUri);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using RichardSzalay.MockHttp;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class CloudFoundryClientTokenResolverTest
|
||||
{
|
||||
[Fact]
|
||||
public void ClientTokenResolverRequiresOptions()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new CloudFoundryClientTokenResolver(null));
|
||||
Assert.Equal("options", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ClientTokenResolver_ReturnsAccessToken_OnSuccess()
|
||||
{
|
||||
// arrange
|
||||
var options = new CloudFoundryOptions() { AccessTokenEndpoint = "/tokenUrl", AuthorizationUrl = "http://localhost", ClientId = "validId", ClientSecret = "clientSecret" };
|
||||
var resolver = new CloudFoundryClientTokenResolver(options, GetMockHttpClient());
|
||||
var expectedToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI3YTMzYzVhNjhjY2I0YjRiYmQ5N2I4MTRlZWExMTc3MiIsInN1YiI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsInNjb3BlIjpbInRlc3Rncm91cCIsIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJhenAiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsIm9yaWdpbiI6InVhYSIsInVzZXJfbmFtZSI6ImRhdmUiLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsImF1dGhfdGltZSI6MTU0NjY0MjkzMywicmV2X3NpZyI6ImE1ZWY2ODg5IiwiaWF0IjoxNTQ2NjQyOTM1LCJleHAiOjE1NDY2ODYxMzUsImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsImF1ZCI6WyJvcGVuaWQiLCJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiXX0.tGTXZzuuUSObTwdPHSx-zvnld20DH5hlOZlYp5DhjwkMIsZB0uIvVwbVDkPp7H_AmmeJoo6vqa5hbbgfgnYpTrKlCGOypnHoa3yRIKrwcDmLLujaMz6ApZeaJ7sJN-0N1UnPZ9iGcqvt9hNb_198zRnMXGH72oI0e2iGUBV1olCFVdZTnMGT7sUieDFKy7n0ghZYq_gUI8rfvTwiC3lfxv0nDXz4oE9Z-UKhK6q1zkAtQrz61FQ_CHONejz1JnuxQFKMMvm8JLcRkn6OL-EcSi1hkmFw0efO1OqccQacxphlafyHloVPQ3IOtzLjCf8sJ5NgTdCTC3iddT_sYovdrg";
|
||||
|
||||
// act
|
||||
var token = await resolver.GetAccessToken();
|
||||
|
||||
// assert
|
||||
Assert.Equal(expectedToken, token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ClientTokenResolver_Throws_OnRemoteFail()
|
||||
{
|
||||
// arrange
|
||||
var options = new CloudFoundryOptions() { AccessTokenEndpoint = "/tokenUrl", AuthorizationUrl = "http://localhost", ClientId = "badId", ClientSecret = "clientSecret" };
|
||||
var resolver = new CloudFoundryClientTokenResolver(options, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var tokenError = await Assert.ThrowsAsync<Exception>(() => resolver.GetAccessToken());
|
||||
|
||||
// assert
|
||||
Assert.Contains("OAuth token endpoint failure: ", tokenError.Message);
|
||||
}
|
||||
|
||||
private readonly string realTokenResponse = "{'access_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI3YTMzYzVhNjhjY2I0YjRiYmQ5N2I4MTRlZWExMTc3MiIsInN1YiI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsInNjb3BlIjpbInRlc3Rncm91cCIsIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJhenAiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsIm9yaWdpbiI6InVhYSIsInVzZXJfbmFtZSI6ImRhdmUiLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsImF1dGhfdGltZSI6MTU0NjY0MjkzMywicmV2X3NpZyI6ImE1ZWY2ODg5IiwiaWF0IjoxNTQ2NjQyOTM1LCJleHAiOjE1NDY2ODYxMzUsImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsImF1ZCI6WyJvcGVuaWQiLCJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiXX0.tGTXZzuuUSObTwdPHSx-zvnld20DH5hlOZlYp5DhjwkMIsZB0uIvVwbVDkPp7H_AmmeJoo6vqa5hbbgfgnYpTrKlCGOypnHoa3yRIKrwcDmLLujaMz6ApZeaJ7sJN-0N1UnPZ9iGcqvt9hNb_198zRnMXGH72oI0e2iGUBV1olCFVdZTnMGT7sUieDFKy7n0ghZYq_gUI8rfvTwiC3lfxv0nDXz4oE9Z-UKhK6q1zkAtQrz61FQ_CHONejz1JnuxQFKMMvm8JLcRkn6OL-EcSi1hkmFw0efO1OqccQacxphlafyHloVPQ3IOtzLjCf8sJ5NgTdCTC3iddT_sYovdrg','token_type':'bearer','id_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJzdWIiOiI5NWJiYjM0Ni1iNjhjLTRmMTUtYjM0MS03MGQ2MGY5ZjQ2YmEiLCJhdWQiOlsiYzkyMGI0ZjUtNDg3Yy00ZGQwLWE2M2QtNGQ0MGMxMzMxOTg2Il0sImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsImV4cCI6MTU0NjY4NjEzNSwiaWF0IjoxNTQ2NjQyOTM1LCJhbXIiOlsicHdkIl0sImF6cCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsIm9yaWdpbiI6InVhYSIsImp0aSI6IjdhMzNjNWE2OGNjYjRiNGJiZDk3YjgxNGVlYTExNzcyIiwicHJldmlvdXNfbG9nb25fdGltZSI6MTU0NjYzMzU1NjA0NCwiZW1haWxfdmVyaWZpZWQiOnRydWUsImNsaWVudF9pZCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsImNpZCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJ1c2VyX25hbWUiOiJkYXZlIiwicmV2X3NpZyI6ImE1ZWY2ODg5IiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsImF1dGhfdGltZSI6MTU0NjY0MjkzM30.KkTVOTg7Bhj1EWO63QmWzjnEAnKesoSLGfGL-2Y19PiK62KRd66dOcVcQEA_nWIE1mJQZsDByQYwcEuVRAiP-mXY0L2MrWUnRlW5yn1fqOc44iSDggMF5VfjGQok8fGfBPQX7va0evfaOaulRMuWsijYvzZtV-KncGUpxGwkzRs2AAEbkAv1_vAD2zSGJ-ji5L7s4a2-Qc_LxDlNANoYllzMTVxZ2DSvVPLfKPNGgSvNC7t053ExfGRXk-6cPxVznkngWDlYALeXsnrbvXdjuk1dw8dcXRhL4PUJDI7EVvTdqzd1fPYRgAQ3KJZOmvzBY7bxFtoq9odmKKHTI4CFUQ','refresh_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI4NjdiNTcxNTBlNmM0ZDQ3OTM0NmE3ZjgwMTJmMGY2My1yIiwic3ViIjoiOTViYmIzNDYtYjY4Yy00ZjE1LWIzNDEtNzBkNjBmOWY0NmJhIiwic2NvcGUiOlsidGVzdGdyb3VwIiwib3BlbmlkIl0sImlhdCI6MTU0NjY0MjkzNSwiZXhwIjoxNTQ5MjM0OTM1LCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJpc3MiOiJodHRwczovL3N0ZWVsdG9lLnVhYS5jZi5iZWV0LnNwcmluZ2FwcHMuaW8vb2F1dGgvdG9rZW4iLCJ6aWQiOiIzYTNlYWRhZC01YjJmLTQ1MzAtYWY5NS1hNjliYzBhZmQxNWIiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9uYW1lIjoiZGF2ZSIsIm9yaWdpbiI6InVhYSIsInVzZXJfaWQiOiI5NWJiYjM0Ni1iNjhjLTRmMTUtYjM0MS03MGQ2MGY5ZjQ2YmEiLCJyZXZfc2lnIjoiYTVlZjY4ODkiLCJhdWQiOlsib3BlbmlkIiwiYzkyMGI0ZjUtNDg3Yy00ZGQwLWE2M2QtNGQ0MGMxMzMxOTg2Il19.cOHCiZgmbtN1uyuvEou879du5XVvJqHEmkf6-2G0V9I1qYy0PJbGMHNL32d6L9LVsnXs4iTfi1qpKfdEGbfZbX8rNwNwcRoPndOZBepTTZHJPcU49VEF4t1hZ5icbt_4mSiQwpNy2n_mqwHizV8BAeOeQc_zUE-YFiuQC6V3ac0rTYO4NJGSioSG_y6HFp3refiPZVPT7En-dwhd-Yic1SB1OPxQ5bRNS7AAjGLeeCWOZQVxwQa5Atv9t791yUkH1zX8Psh_LRy2O8E6o7IBLI_yjz5N-YIHRa-BuMPDdKWarcOjy1KKJM27HynAQo1T4J0Ft8hSsIxFlObTd3-FVQ','expires_in':43199,'scope':'testgroup openid','jti':'7a33c5a68ccb4b4bbd97b814eea11772'}";
|
||||
|
||||
private HttpClient GetMockHttpClient()
|
||||
{
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
|
||||
mockHttp
|
||||
.When("http://localhost/tokenUrl")
|
||||
.WithFormData("client_id", "validId")
|
||||
.Respond("application/json", realTokenResponse); // Respond with JSON
|
||||
mockHttp
|
||||
.When("http://localhost/tokenUrl")
|
||||
.WithFormData("client_id", "badId")
|
||||
.Respond(HttpStatusCode.BadRequest, new StringContent(string.Empty)); // Respond with JSON
|
||||
|
||||
return mockHttp.ToHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class CloudFoundryJwtTest
|
||||
{
|
||||
[Fact]
|
||||
public void ClaimsReMapped_WhenPresent()
|
||||
{
|
||||
// arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim("client_id", "clientId"),
|
||||
new Claim("user_id", "nameId"),
|
||||
new Claim("given_name", "First_Name"),
|
||||
new Claim("family_name", "Last_Name"),
|
||||
};
|
||||
var jwt = new JwtSecurityToken(issuer: "uaa");
|
||||
var identity = new ClaimsIdentity(claims);
|
||||
|
||||
// act
|
||||
CloudFoundryJwt.OnTokenValidatedAddClaims(identity, jwt);
|
||||
|
||||
// assert
|
||||
Assert.Contains(identity.Claims, c => c.Type == ClaimTypes.NameIdentifier && c.Value == "nameId");
|
||||
Assert.Contains(identity.Claims, c => c.Type == ClaimTypes.GivenName && c.Value == "First_Name");
|
||||
Assert.Contains(identity.Claims, c => c.Type == ClaimTypes.Surname && c.Value == "Last_Name");
|
||||
Assert.DoesNotContain(identity.Claims, c => c.Type == ClaimTypes.Email);
|
||||
Assert.Contains(identity.Claims, c => c.Type == ClaimTypes.Name && c.Value == "clientId");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Steeltoe.CloudFoundry.Connector.Services;
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class CloudFoundryOptionsConfigurerTest
|
||||
{
|
||||
[Fact]
|
||||
public void Configure_NoOptions_Throws()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => CloudFoundryOptionsConfigurer.Configure(null, null));
|
||||
Assert.Equal("options", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_NoServiceInfo_ReturnsDefaults()
|
||||
{
|
||||
// arrange
|
||||
var opts = new CloudFoundryOptions();
|
||||
string authURL = "http://" + CloudFoundryDefaults.OAuthServiceUrl;
|
||||
|
||||
// act
|
||||
CloudFoundryOptionsConfigurer.Configure(null, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal(authURL, opts.AuthorizationUrl);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientId, opts.ClientId);
|
||||
Assert.Equal(CloudFoundryDefaults.ClientSecret, opts.ClientSecret);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.CheckTokenUri, opts.TokenInfoUrl);
|
||||
Assert.True(opts.ValidateAudience);
|
||||
Assert.True(opts.ValidateCertificates);
|
||||
Assert.True(opts.ValidateIssuer);
|
||||
Assert.True(opts.ValidateLifetime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_WithServiceInfo_ReturnsExpected()
|
||||
{
|
||||
// arrange
|
||||
string authURL = "http://domain";
|
||||
var opts = new CloudFoundryOptions();
|
||||
SsoServiceInfo info = new SsoServiceInfo("foobar", "clientId", "secret", "http://domain");
|
||||
|
||||
// act
|
||||
CloudFoundryOptionsConfigurer.Configure(info, opts);
|
||||
|
||||
// assert
|
||||
Assert.Equal("clientId", opts.ClientId);
|
||||
Assert.Equal("secret", opts.ClientSecret);
|
||||
Assert.Equal(authURL + CloudFoundryDefaults.CheckTokenUri, opts.TokenInfoUrl);
|
||||
Assert.True(opts.ValidateCertificates);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class CloudFoundryOptionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void ParameterlessConstructor_StillReadsEnvVars()
|
||||
{
|
||||
// arrange
|
||||
Environment.SetEnvironmentVariable("sso_auth_domain", "auth_domain");
|
||||
Environment.SetEnvironmentVariable("sso_client_id", "ssoClientId");
|
||||
Environment.SetEnvironmentVariable("sso_client_secret", "ssoClientSecret");
|
||||
|
||||
// act
|
||||
var options = new CloudFoundryOptions();
|
||||
|
||||
// assert
|
||||
Assert.Equal("auth_domain", options.AuthorizationUrl);
|
||||
Assert.Equal("ssoClientId", options.ClientId);
|
||||
Assert.Equal("ssoClientSecret", options.ClientSecret);
|
||||
|
||||
// reset the env
|
||||
Environment.SetEnvironmentVariable("sso_auth_domain", string.Empty);
|
||||
Environment.SetEnvironmentVariable("sso_client_id", string.Empty);
|
||||
Environment.SetEnvironmentVariable("sso_client_secret", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RichardSzalay.MockHttp;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class CloudFoundryTokenKeyResolverTest
|
||||
{
|
||||
private readonly string tokenKeysJsonString = @"{'keys':[{'kty':'RSA','e':'AQAB','use':'sig','kid':'key-1','alg':'RS256','value':'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyyt7z23ctJP3UStbx0b/\nLbLWbQfEfOAHh09x7BnFB7vLw4cngzy685LrQyi12FKHDx+0Ux3B4o5hvfHLmgml\npXrWUjf0G3aE77FfMhulmIS9avlNjSUErO7Tgq4/XYiasfWeiIfjhs4cQvOwZeF3\nSBlj5VdHakLertr2DAIigmlziCOkb4is67dfGLZkc8UeKTJmueW56jlT9hyCRjBM\nGbV9LNiaZ6vp+jwk2ugW0pIjMbyfMxoIExiMQmYwT0TP/n8cd89eaKqmO2HXiYL9\nyqqTMMCV6I2lXNxXCEu/cii7kj9Il4aLowzWHJ0Z4XPJsTufV8uZShYxV+gBSekM\nLwIDAQAB\n-----END PUBLIC KEY-----','n':'AMsre89t3LST91ErW8dG_y2y1m0HxHzgB4dPcewZxQe7y8OHJ4M8uvOS60MotdhShw8ftFMdweKOYb3xy5oJpaV61lI39Bt2hO-xXzIbpZiEvWr5TY0lBKzu04KuP12ImrH1noiH44bOHELzsGXhd0gZY-VXR2pC3q7a9gwCIoJpc4gjpG-IrOu3Xxi2ZHPFHikyZrnlueo5U_YcgkYwTBm1fSzYmmer6fo8JNroFtKSIzG8nzMaCBMYjEJmME9Ez_5_HHfPXmiqpjth14mC_cqqkzDAleiNpVzcVwhLv3Iou5I_SJeGi6MM1hydGeFzybE7n1fLmUoWMVfoAUnpDC8'}]}";
|
||||
private readonly CloudFoundryOptions happyPathOptions = new CloudFoundryOptions() { AuthorizationUrl = "http://localhost" };
|
||||
private readonly CloudFoundryOptions networkFailOptions = new CloudFoundryOptions() { AuthorizationUrl = "http://localhost:81" };
|
||||
private readonly CloudFoundryOptions serviceUnavailableOptions = new CloudFoundryOptions() { AuthorizationUrl = "http://localhost:82" };
|
||||
|
||||
[Fact]
|
||||
public void TokenKeyResolver_Requires_Options()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new CloudFoundryTokenKeyResolver(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSigningKey_ReturnsKey_FromServer()
|
||||
{
|
||||
// arrange a resolver that succeeds
|
||||
var tkr = new CloudFoundryTokenKeyResolver(happyPathOptions, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var expected = tkr.ResolveSigningKey(string.Empty, null, "key-1", happyPathOptions.TokenValidationParameters);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(expected);
|
||||
var tokenKey = expected.First();
|
||||
Assert.IsType<JsonWebKey>(tokenKey);
|
||||
Assert.Equal("key-1", tokenKey.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSigningKey_ReturnsKeyPreviouslyResolved()
|
||||
{
|
||||
// arrange a resolver that has previously retrieved keys, but will fail going forward
|
||||
var tkr = new CloudFoundryTokenKeyResolver(happyPathOptions, GetMockHttpClient());
|
||||
tkr.ResolveSigningKey(string.Empty, null, "key-1", happyPathOptions.TokenValidationParameters);
|
||||
tkr.Options = networkFailOptions;
|
||||
Assert.Equal(networkFailOptions.AuthorizationUrl, tkr.Options.AuthorizationUrl);
|
||||
|
||||
// act
|
||||
var expected = tkr.ResolveSigningKey(string.Empty, null, "key-1", happyPathOptions.TokenValidationParameters);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(expected);
|
||||
var tokenKey = expected.First();
|
||||
Assert.IsType<JsonWebKey>(tokenKey);
|
||||
Assert.Equal("key-1", tokenKey.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSigningKey_ReturnsNull_WhenNoKeyFound()
|
||||
{
|
||||
// arrange
|
||||
var tkr = new CloudFoundryTokenKeyResolver(serviceUnavailableOptions, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var expected = tkr.ResolveSigningKey(string.Empty, null, "key-1", serviceUnavailableOptions.TokenValidationParameters);
|
||||
|
||||
// assert
|
||||
Assert.Null(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void FetchKeySet_Throws_OnHttpClientException()
|
||||
{
|
||||
// arrange
|
||||
var tkr = new CloudFoundryTokenKeyResolver(networkFailOptions, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var exception = await Assert.ThrowsAsync<HttpRequestException>(() => tkr.FetchKeySet());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void FetchKeySet_ReturnsNull_OnFailure()
|
||||
{
|
||||
// arrange
|
||||
var tkr = new CloudFoundryTokenKeyResolver(serviceUnavailableOptions, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var expected = await tkr.FetchKeySet();
|
||||
|
||||
// assert
|
||||
Assert.Null(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void FetchKeySet_ReturnsKeySet_OnSuccess()
|
||||
{
|
||||
// arrange
|
||||
var tkr = new CloudFoundryTokenKeyResolver(happyPathOptions, GetMockHttpClient());
|
||||
|
||||
// act
|
||||
var expected = await tkr.FetchKeySet();
|
||||
|
||||
// assert
|
||||
Assert.Contains(expected.Keys, key => key.Kid == "key-1");
|
||||
var tokenKey = expected.Keys.First();
|
||||
Assert.Equal("RS256", tokenKey.Alg);
|
||||
Assert.Equal("sig", tokenKey.Use);
|
||||
Assert.Equal("AQAB", tokenKey.E);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJsonWebKeySet_Parses_JsonString()
|
||||
{
|
||||
// arrange
|
||||
var tkr = new CloudFoundryTokenKeyResolver(happyPathOptions);
|
||||
|
||||
// act
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var expected = tkr.GetJsonWebKeySet(tokenKeysJsonString);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
// assert
|
||||
Assert.Contains(expected.Keys, key => key.Kid == "key-1");
|
||||
var tokenKey = expected.Keys.First();
|
||||
Assert.Equal("RS256", tokenKey.Alg);
|
||||
Assert.Equal("sig", tokenKey.Use);
|
||||
Assert.Equal("AQAB", tokenKey.E);
|
||||
}
|
||||
|
||||
private HttpClient GetMockHttpClient()
|
||||
{
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
|
||||
mockHttp
|
||||
.When("http://localhost:81" + CloudFoundryDefaults.JwtTokenUri)
|
||||
.Throw(new HttpRequestException()); // emulate a DNS failure
|
||||
mockHttp
|
||||
.When("http://localhost:82" + CloudFoundryDefaults.JwtTokenUri)
|
||||
.Respond(HttpStatusCode.ServiceUnavailable); // Respond unavailable
|
||||
mockHttp
|
||||
.When("http://localhost" + CloudFoundryDefaults.JwtTokenUri)
|
||||
.Respond("application/json", tokenKeysJsonString); // Respond with JSON
|
||||
|
||||
return mockHttp.ToHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RichardSzalay.MockHttp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.ServiceModel.Web;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class JwtAuthorizationManagerTest
|
||||
{
|
||||
private readonly CloudFoundryOptions _options = new CloudFoundryOptions() { AuthorizationUrl = "http://localhost" };
|
||||
private readonly string tokenKeysJsonString = @"{'keys':[{'kty':'RSA','e':'AQAB','use':'sig','kid':'key-1','alg':'RS256','value':'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyyt7z23ctJP3UStbx0b/\nLbLWbQfEfOAHh09x7BnFB7vLw4cngzy685LrQyi12FKHDx+0Ux3B4o5hvfHLmgml\npXrWUjf0G3aE77FfMhulmIS9avlNjSUErO7Tgq4/XYiasfWeiIfjhs4cQvOwZeF3\nSBlj5VdHakLertr2DAIigmlziCOkb4is67dfGLZkc8UeKTJmueW56jlT9hyCRjBM\nGbV9LNiaZ6vp+jwk2ugW0pIjMbyfMxoIExiMQmYwT0TP/n8cd89eaKqmO2HXiYL9\nyqqTMMCV6I2lXNxXCEu/cii7kj9Il4aLowzWHJ0Z4XPJsTufV8uZShYxV+gBSekM\nLwIDAQAB\n-----END PUBLIC KEY-----','n':'AMsre89t3LST91ErW8dG_y2y1m0HxHzgB4dPcewZxQe7y8OHJ4M8uvOS60MotdhShw8ftFMdweKOYb3xy5oJpaV61lI39Bt2hO-xXzIbpZiEvWr5TY0lBKzu04KuP12ImrH1noiH44bOHELzsGXhd0gZY-VXR2pC3q7a9gwCIoJpc4gjpG-IrOu3Xxi2ZHPFHikyZrnlueo5U_YcgkYwTBm1fSzYmmer6fo8JNroFtKSIzG8nzMaCBMYjEJmME9Ez_5_HHfPXmiqpjth14mC_cqqkzDAleiNpVzcVwhLv3Iou5I_SJeGi6MM1hydGeFzybE7n1fLmUoWMVfoAUnpDC8'},{'kty':'RSA','e':'AQAB','use':'sig','kid':'key-2','alg':'RS256','value':'some super simple key that just happens to have enough characters','n':'someNvalue'}]}";
|
||||
|
||||
[Fact]
|
||||
public void JwtAuthorizationManager_Requires_Options()
|
||||
{
|
||||
// arrange
|
||||
var manager = new JwtAuthorizationManager();
|
||||
|
||||
// act
|
||||
var exception = Assert.Throws<WebFaultException<string>>(() => manager.GetPrincipalFromRequestHeaders(null));
|
||||
|
||||
// assert
|
||||
Assert.Equal("SSO Configuration is missing", exception.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtAuthorizationManager_Requires_AuthorizationHeader()
|
||||
{
|
||||
// arrange
|
||||
var manager = new JwtAuthorizationManager(_options);
|
||||
var headers = new WebHeaderCollection();
|
||||
|
||||
// act
|
||||
var exception = Assert.Throws<WebFaultException<string>>(() => manager.GetPrincipalFromRequestHeaders(headers));
|
||||
|
||||
// assert
|
||||
Assert.Equal("No Authorization header", exception.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtAuthorizationManager_Requires_BearerTokenFormat()
|
||||
{
|
||||
// arrange
|
||||
var manager = new JwtAuthorizationManager(_options);
|
||||
var headers = new WebHeaderCollection
|
||||
{
|
||||
{ HttpRequestHeader.Authorization, "bear" }
|
||||
};
|
||||
|
||||
// act
|
||||
var exception = Assert.Throws<WebFaultException<string>>(() => manager.GetPrincipalFromRequestHeaders(headers));
|
||||
|
||||
// assert
|
||||
Assert.Equal("Wrong Token Format", exception.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtAuthorizationManager_Requires_An_Actual_Token()
|
||||
{
|
||||
// arrange
|
||||
var manager = new JwtAuthorizationManager(_options);
|
||||
var headers = new WebHeaderCollection
|
||||
{
|
||||
{ HttpRequestHeader.Authorization, "Bearer " }
|
||||
};
|
||||
|
||||
// act
|
||||
var exception = Assert.Throws<WebFaultException<string>>(() => manager.GetPrincipalFromRequestHeaders(headers));
|
||||
|
||||
// assert
|
||||
Assert.Equal("No Token", exception.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtAuthorizationManager_ThrowsInvalidToken()
|
||||
{
|
||||
// arrange
|
||||
_options.TokenKeyResolver = null;
|
||||
_options.TokenKeyResolver = new CloudFoundry.CloudFoundryTokenKeyResolver(_options.AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri, GetMockHttpMessageHandler(), false);
|
||||
_options.TokenValidator.Options = _options;
|
||||
_options.TokenValidationParameters = null;
|
||||
_options.TokenValidationParameters = _options.GetTokenValidationParameters();
|
||||
var manager = new JwtAuthorizationManager(_options);
|
||||
var headers = new WebHeaderCollection
|
||||
{
|
||||
{ HttpRequestHeader.Authorization, "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI3YTMzYzVhNjhjY2I0YjRiYmQ5N2I4MTRlZWExMTc3MiIsInN1YiI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsInNjb3BlIjpbInRlc3Rncm91cCIsIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJhenAiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsIm9yaWdpbiI6InVhYSIsInVzZXJfbmFtZSI6ImRhdmUiLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsImF1dGhfdGltZSI6MTU0NjY0MjkzMywicmV2X3NpZyI6ImE1ZWY2ODg5IiwiaWF0IjoxNTQ2NjQyOTM1LCJleHAiOjE1NDY2ODYxMzUsImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsImF1ZCI6WyJvcGVuaWQiLCJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiXX0.tGTXZzuuUSObTwdPHSx-zvnld20DH5hlOZlYp5DhjwkMIsZB0uIvVwbVDkPp7H_AmmeJoo6vqa5hbbgfgnYpTrKlCGOypnHoa3yRIKrwcDmLLujaMz6ApZeaJ7sJN-0N1UnPZ9iGcqvt9hNb_198zRnMXGH72oI0e2iGUBV1olCFVdZTnMGT7sUieDFKy7n0ghZYq_gUI8rfvTwiC3lfxv0nDXz4oE9Z-UKhK6q1zkAtQrz61FQ_CHONejz1JnuxQFKMMvm8JLcRkn6OL-EcSi1hkmFw0efO1OqccQacxphlafyHloVPQ3IOtzLjCf8sJ5NgTdCTC3iddT_sYovdrg" }
|
||||
};
|
||||
|
||||
// act
|
||||
var exception = Assert.Throws<WebFaultException<string>>(() => manager.GetPrincipalFromRequestHeaders(headers));
|
||||
|
||||
// assert
|
||||
Assert.StartsWith("IDX10223: Lifetime validation failed", exception.Detail);
|
||||
}
|
||||
|
||||
[Fact(Skip = "fails at signature validation")]
|
||||
public void JwtAuthorizationManager_ReturnsPrincipalFromToken()
|
||||
{
|
||||
// arrange
|
||||
_options.TokenKeyResolver = null;
|
||||
_options.TokenKeyResolver = new CloudFoundry.CloudFoundryTokenKeyResolver(_options.AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri, GetMockHttpMessageHandler(), false);
|
||||
_options.TokenValidator.Options = _options;
|
||||
_options.TokenValidationParameters = null;
|
||||
_options.TokenValidationParameters = _options.GetTokenValidationParameters();
|
||||
var manager = new JwtAuthorizationManager(_options);
|
||||
var headers = new WebHeaderCollection
|
||||
{
|
||||
{ HttpRequestHeader.Authorization, $"Bearer {CreateJwt()}" }
|
||||
};
|
||||
|
||||
// act
|
||||
var principal = manager.GetPrincipalFromRequestHeaders(headers);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(principal);
|
||||
Assert.Equal("dave", principal.Identity.Name);
|
||||
}
|
||||
|
||||
private HttpMessageHandler GetMockHttpMessageHandler()
|
||||
{
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
|
||||
mockHttp
|
||||
.When(HttpMethod.Get, _options.AuthorizationUrl + CloudFoundryDefaults.JwtTokenUri)
|
||||
.Respond("application/json", tokenKeysJsonString); // Respond with JSON
|
||||
|
||||
return mockHttp;
|
||||
}
|
||||
|
||||
private string CreateJwt()
|
||||
{
|
||||
var signingKey = "some super simple key that just happens to have enough characters";
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
|
||||
var header = new JwtHeader(new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature))
|
||||
{
|
||||
{ "kid", "key-2" }
|
||||
};
|
||||
var payload = new JwtPayload("uaa", "tests", new List<Claim> { new Claim("scope", "openid") }, null, DateTime.Now.AddMinutes(5));
|
||||
var secToken = new JwtSecurityToken(header, payload);
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.WriteToken(secToken);
|
||||
return token;
|
||||
|
||||
// {
|
||||
// { "jti": "7a33c5a68ccb4b4bbd97b814eea11772" },
|
||||
// { "sub": "95bbb346-b68c-4f15-b341-70d60f9f46ba"},
|
||||
// "scope": [ "testgroup", "openid" ],
|
||||
// "client_id": "c920b4f5-487c-4dd0-a63d-4d40c1331986",
|
||||
// "cid": "c920b4f5-487c-4dd0-a63d-4d40c1331986",
|
||||
// "azp": "c920b4f5-487c-4dd0-a63d-4d40c1331986",
|
||||
// "grant_type": "authorization_code",
|
||||
// "user_id": "95bbb346-b68c-4f15-b341-70d60f9f46ba",
|
||||
// "origin": "uaa",
|
||||
// "user_name": "dave",
|
||||
// "email": "dave@testcloud.com",
|
||||
// "auth_time": 1546642933,
|
||||
// "rev_sig": "a5ef6889",
|
||||
// "iat": 1546642935,
|
||||
// "exp": 1546686135,
|
||||
// "iss": "https://steeltoe.uaa.cf.beet.springapps.io/oauth/token",
|
||||
// "zid": "3a3eadad-5b2f-4530-af95-a69bc0afd15b",
|
||||
// "aud": [
|
||||
// "openid",
|
||||
// "c920b4f5-487c-4dd0-a63d-4d40c1331986"
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2017 the original author or authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using Moq;
|
||||
using RichardSzalay.MockHttp;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.ServiceModel.Channels;
|
||||
using Xunit;
|
||||
|
||||
namespace Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test
|
||||
{
|
||||
public class JwtHeaderMessageInspectorTest
|
||||
{
|
||||
[Fact]
|
||||
public void MessageInspector_AttachesUserToken()
|
||||
{
|
||||
// arrange
|
||||
var options = new CloudFoundryOptions() { AuthorizationUrl = "http://localhost", ForwardUserCredentials = true };
|
||||
var inspector = new JwtHeaderMessageInspector(options, "someToken");
|
||||
var properties = new MessageProperties { { HttpRequestMessageProperty.Name, new HttpRequestMessageProperty() } };
|
||||
var message = new Mock<Message>();
|
||||
message.Setup(p => p.Properties).Returns(() => properties);
|
||||
var mo = message.Object;
|
||||
|
||||
// act
|
||||
inspector.BeforeSendRequest(ref mo, null);
|
||||
HttpRequestMessageProperty httpRequestMessage;
|
||||
mo.Properties.TryGetValue(HttpRequestMessageProperty.Name, out object httpRequestMessageObject);
|
||||
httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
|
||||
|
||||
// assert
|
||||
Assert.True(httpRequestMessage.Headers.AllKeys.Any());
|
||||
Assert.Equal("Bearer someToken", httpRequestMessage.Headers["Authorization"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessageInspector_GetsAndAttachesOwnToken()
|
||||
{
|
||||
// arrange
|
||||
var options = new CloudFoundryOptions() { AccessTokenEndpoint = "/tokenUrl", AuthorizationUrl = "http://localhost", ClientId = "validId", ClientSecret = "validSecret" };
|
||||
var inspector = new JwtHeaderMessageInspector(options, null, GetMockHttpClient());
|
||||
var properties = new MessageProperties { { HttpRequestMessageProperty.Name, new HttpRequestMessageProperty() } };
|
||||
var message = new Mock<Message>();
|
||||
message.Setup(p => p.Properties).Returns(() => properties);
|
||||
var mo = message.Object;
|
||||
|
||||
// act
|
||||
inspector.BeforeSendRequest(ref mo, null);
|
||||
HttpRequestMessageProperty httpRequestMessage;
|
||||
mo.Properties.TryGetValue(HttpRequestMessageProperty.Name, out object httpRequestMessageObject);
|
||||
httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
|
||||
|
||||
// assert
|
||||
Assert.True(httpRequestMessage.Headers.AllKeys.Any());
|
||||
Assert.Equal("Bearer someClientCredentialsToken", httpRequestMessage.Headers["Authorization"]);
|
||||
}
|
||||
|
||||
private readonly string realTokenResponse = "{'access_token':'someClientCredentialsToken','token_type':'bearer','id_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJzdWIiOiI5NWJiYjM0Ni1iNjhjLTRmMTUtYjM0MS03MGQ2MGY5ZjQ2YmEiLCJhdWQiOlsiYzkyMGI0ZjUtNDg3Yy00ZGQwLWE2M2QtNGQ0MGMxMzMxOTg2Il0sImlzcyI6Imh0dHBzOi8vc3RlZWx0b2UudWFhLmNmLmJlZXQuc3ByaW5nYXBwcy5pby9vYXV0aC90b2tlbiIsImV4cCI6MTU0NjY4NjEzNSwiaWF0IjoxNTQ2NjQyOTM1LCJhbXIiOlsicHdkIl0sImF6cCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJlbWFpbCI6ImRhdmVAdGVzdGNsb3VkLmNvbSIsInppZCI6IjNhM2VhZGFkLTViMmYtNDUzMC1hZjk1LWE2OWJjMGFmZDE1YiIsIm9yaWdpbiI6InVhYSIsImp0aSI6IjdhMzNjNWE2OGNjYjRiNGJiZDk3YjgxNGVlYTExNzcyIiwicHJldmlvdXNfbG9nb25fdGltZSI6MTU0NjYzMzU1NjA0NCwiZW1haWxfdmVyaWZpZWQiOnRydWUsImNsaWVudF9pZCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsImNpZCI6ImM5MjBiNGY1LTQ4N2MtNGRkMC1hNjNkLTRkNDBjMTMzMTk4NiIsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJ1c2VyX25hbWUiOiJkYXZlIiwicmV2X3NpZyI6ImE1ZWY2ODg5IiwidXNlcl9pZCI6Ijk1YmJiMzQ2LWI2OGMtNGYxNS1iMzQxLTcwZDYwZjlmNDZiYSIsImF1dGhfdGltZSI6MTU0NjY0MjkzM30.KkTVOTg7Bhj1EWO63QmWzjnEAnKesoSLGfGL-2Y19PiK62KRd66dOcVcQEA_nWIE1mJQZsDByQYwcEuVRAiP-mXY0L2MrWUnRlW5yn1fqOc44iSDggMF5VfjGQok8fGfBPQX7va0evfaOaulRMuWsijYvzZtV-KncGUpxGwkzRs2AAEbkAv1_vAD2zSGJ-ji5L7s4a2-Qc_LxDlNANoYllzMTVxZ2DSvVPLfKPNGgSvNC7t053ExfGRXk-6cPxVznkngWDlYALeXsnrbvXdjuk1dw8dcXRhL4PUJDI7EVvTdqzd1fPYRgAQ3KJZOmvzBY7bxFtoq9odmKKHTI4CFUQ','refresh_token':'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiI4NjdiNTcxNTBlNmM0ZDQ3OTM0NmE3ZjgwMTJmMGY2My1yIiwic3ViIjoiOTViYmIzNDYtYjY4Yy00ZjE1LWIzNDEtNzBkNjBmOWY0NmJhIiwic2NvcGUiOlsidGVzdGdyb3VwIiwib3BlbmlkIl0sImlhdCI6MTU0NjY0MjkzNSwiZXhwIjoxNTQ5MjM0OTM1LCJjaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJjbGllbnRfaWQiOiJjOTIwYjRmNS00ODdjLTRkZDAtYTYzZC00ZDQwYzEzMzE5ODYiLCJpc3MiOiJodHRwczovL3N0ZWVsdG9lLnVhYS5jZi5iZWV0LnNwcmluZ2FwcHMuaW8vb2F1dGgvdG9rZW4iLCJ6aWQiOiIzYTNlYWRhZC01YjJmLTQ1MzAtYWY5NS1hNjliYzBhZmQxNWIiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9uYW1lIjoiZGF2ZSIsIm9yaWdpbiI6InVhYSIsInVzZXJfaWQiOiI5NWJiYjM0Ni1iNjhjLTRmMTUtYjM0MS03MGQ2MGY5ZjQ2YmEiLCJyZXZfc2lnIjoiYTVlZjY4ODkiLCJhdWQiOlsib3BlbmlkIiwiYzkyMGI0ZjUtNDg3Yy00ZGQwLWE2M2QtNGQ0MGMxMzMxOTg2Il19.cOHCiZgmbtN1uyuvEou879du5XVvJqHEmkf6-2G0V9I1qYy0PJbGMHNL32d6L9LVsnXs4iTfi1qpKfdEGbfZbX8rNwNwcRoPndOZBepTTZHJPcU49VEF4t1hZ5icbt_4mSiQwpNy2n_mqwHizV8BAeOeQc_zUE-YFiuQC6V3ac0rTYO4NJGSioSG_y6HFp3refiPZVPT7En-dwhd-Yic1SB1OPxQ5bRNS7AAjGLeeCWOZQVxwQa5Atv9t791yUkH1zX8Psh_LRy2O8E6o7IBLI_yjz5N-YIHRa-BuMPDdKWarcOjy1KKJM27HynAQo1T4J0Ft8hSsIxFlObTd3-FVQ','expires_in':43199,'scope':'testgroup openid','jti':'7a33c5a68ccb4b4bbd97b814eea11772'}";
|
||||
|
||||
private HttpClient GetMockHttpClient()
|
||||
{
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
|
||||
mockHttp
|
||||
.When("http://localhost/tokenUrl")
|
||||
.WithFormData("client_id", "validId")
|
||||
.Respond("application/json", realTokenResponse); // Respond with JSON
|
||||
|
||||
return mockHttp.ToHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreTestVersion)" />
|
||||
<PackageReference Include="Moq" Version="$(MoqVersion)" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="$(MockHttp)" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitStudioVersion)" />
|
||||
<DotNetCliToolReference Include="dotnet-xunit" Version="$(XunitVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>SA1101;SA1124;SA1201;SA1309;SA1310;SA1401;SA1600;SA1652;1591</NoWarn>
|
||||
<RootNamespace>Steeltoe.Security.Authentication.CloudFoundry.Wcf.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
<Link>stylecop.json</Link>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Steeltoe.Security.Authentication.CloudFoundryWcf\Steeltoe.Security.Authentication.CloudFoundryWcf.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceModel" />
|
||||
<Reference Include="System.ServiceModel.Web" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,10 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="$(MockHttp)" />
|
||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersVersion)" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\versions.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
Загрузка…
Ссылка в новой задаче