I am trying to implement PersistedGrantStore on mongodb. I have seen similar questions and answers but no luck so far (How can I implement PersistedGrantStore on my mongodb database).
I have created a class inhertied from IPersistedGrantStore I have inject it in DI using AddTransient, but still no call is made within my class. Here is a section of my code within ConfigureServices(IServiceCollection services) function of startup.cs
services.AddAuthentication(o =>
{
o.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
o.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(options =>
{
options.SaveToken = true;
options.Authority = authorityServerURL;// "https://demo.identityserver.io";
//options.ApiName = "BWalle_API";
//options.ApiSecret = "Odsdffegfgdfgdfglq_";
options.RequireHttpsMetadata = false;
options.EnableCaching = false;
options.SupportedTokens = SupportedTokens.Jwt;
});
var builder = services.AddIdentityServer(options =>
{
options.Endpoints.EnableUserInfoEndpoint = true;
options.Events = new EventsOptions()
{
RaiseErrorEvents = true,
RaiseFailureEvents = true,
RaiseInformationEvents = true,
RaiseSuccessEvents = true
};
})
.AddTestUsers(Config.GetUsers())
.AddSigningCredential(new Microsoft.IdentityModel.Tokens.SigningCredentials(GetSecurityKey(), SecurityAlgorithms.RsaSha512Signature))
.AddResourceStore<ResourceStore>()
.AddClientStore<ClientStore>()
.AddProfileService<MongoDbProfileService>()
.AddResourceOwnerValidator<MongoDbResourceOwnerPasswordValidator>()
.AddJwtBearerClientAuthentication();
builder.Services.AddTransient<IPersistedGrantStore, PersistedGrantStore>();
And this is the ClientStore Class:
public class ClientStore : IClientStore
{
Task<Client> IClientStore.FindClientByIdAsync(string clientId)
{
Client client = new Client
{
ClientId = "BWalle_API",
ClientName = "BWalle API Client",
//AllowAccessTokensViaBrowser = true,
//AlwaysSendClientClaims = true,
AllowedGrantTypes = new List<string>() {
GrantType.ResourceOwnerPassword,
GrantType.Hybrid,
GrantType.ClientCredentials
},
ClientSecrets = new List<Secret>
{
new Secret("Odsdffegfgdfgdfglq_".Sha512())
},
AllowedScopes = new List<string>
{
IdentityServer4.IdentityServerConstants.StandardScopes.OpenId,
IdentityServer4.IdentityServerConstants.StandardScopes.Profile,
IdentityServer4.IdentityServerConstants.StandardScopes.Email,
IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
IdentityServer4.IdentityServerConstants.StandardScopes.Phone,
"BWalle_API"
},
Enabled = true,
//AllowedCorsOrigins = new List<string>
//{
// "http://localhost:4200"
//},
AllowOfflineAccess = true,
AllowRememberConsent = false,
AccessTokenType = AccessTokenType.Jwt,
IdentityTokenLifetime = 3600 * 24, // Lifetime to identity token in seconds (defaults to 300 seconds / 5 minutes)
AccessTokenLifetime = 3600 * 24, //3600, // Lifetime of access token in seconds (defaults to 3600 seconds / 1 hour)
AuthorizationCodeLifetime = 3600 * 24, // Lifetime of authorization code in seconds (defaults to 300 seconds / 5 minutes)
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Sliding,
UpdateAccessTokenClaimsOnRefresh = true,
IncludeJwtId = true
};
return Task.FromResult<Client>(client);
}
}
And this is the PersistedGrantStore Class:
public class PersistedGrantStore : IPersistedGrantStore
{
private readonly IAppRepository appRepository;
public PersistedGrantStore(IAppRepository DBAppRepository)
{
this.appRepository = DBAppRepository;
}
public Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
{
throw new NotImplementedException();
}
public Task<PersistedGrant> GetAsync(string key)
{
throw new NotImplementedException();
}
public Task RemoveAllAsync(string subjectId, string clientId)
{
throw new NotImplementedException();
}
public Task RemoveAllAsync(string subjectId, string clientId, string type)
{
throw new NotImplementedException();
}
public Task RemoveAsync(string key)
{
throw new NotImplementedException();
}
public Task StoreAsync(PersistedGrant grant)
{
throw new NotImplementedException();
}
}
I am using those nuget packages:
Identityserver4\2.1.3,
Identityserver4.AccessTokenValidation\2.5.0
Contrib.Microsoft.aspnetcore.identity.mongodb\2.0.5
I have managed successfully to use mongodb to store users and client and now I am trying to store grants instead of using in memory grant stores but no call is made whith in PersistedGrantStore class.
I am using ResourceOwner as GrantType (JWT - Bearer Model).
I can not see what I am missing, any help would be realy helpfull!!!
Solved!!!! What was missing was adding to the scope the 'offline_access' when sending from client to the server the connection request to the endpoint '/connect/token'.
Example of post '/connect/token' with data in body :
client_id=BWalle_API&client_secret=mysecretAPlq_&grant_type=password&scope=BWalle_API offline_access&username=undefined&password=undefined&rememberme=&VerCode=1820-0327-2104-0012
Related
I'm using OWIN 4.2 with .NET Framework 4.7.2 for my ASP.NET MVC client app.
Login works completely fine but logout will fail.
On my client's startup.cs
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "MVC",
ClientSecret = "MVC-Secret",
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
CallbackPath = new PathString("/"),
Scope = "openid api",
SignInAsAuthenticationType = "cookie",
RequireHttpsMetadata = false,
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = OpenIdConnectResponseMode.Query,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to the OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
// generate code verifier and code challenge
var codeVerifier = CryptoRandom.CreateUniqueId(32);
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
// set code_challenge parameter on authorization request
n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
// remember code verifier in cookie (adapted from OWIN nonce cookie)
// see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L85
RememberCodeVerifier(n, codeVerifier);
}
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
return Task.CompletedTask;
},
AuthorizationCodeReceived = n =>
{
// get code verifier from cookie
// see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L102
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier on token request
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
return Task.CompletedTask;
},
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
id,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
}
}
);
I also tried
...
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
PostLogoutRedirectUri = "https://localhost:44347/signout-callback-oidc",
...
And also
...
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
PostLogoutRedirectUri = "https://localhost:44347/",
...
However, all these results in the response
error:invalid_request
error_description:The specified 'post_logout_redirect_uri' is invalid.
error_uri:https://documentation.openiddict.com/errors/ID2052
On my server, the configuration is as follows
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = clientSecret,
DisplayName = displayName,
RedirectUris =
{
new Uri("https://localhost:44347/")
},
Permissions =
{
...
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44347/")
}
}, cancellationToken);
}
I have also tried changing Server config to
PostLogoutRedirectUris =
{
new Uri("https://localhost:44347/signout-callback-oidc")
}
I encountered the same issue, what solved it for me was to add the logout permission in the application - OpenIddictConstants.Permissions.Endpoints.Logout
await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
DisplayName = "MVC client application",
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode
}
});
As answered by Apps in here https://stackoverflow.com/a/69671657/6477254, I can confirm that you must allow permission for the logout endpoint, using the constant value of OpenIddictConstants.Permissions.Endpoints.Logout, which hold "ept:logout" string value when creating the OpenIddict data.
I am having some issues getting swagger to work with oauth2.
I have created a client in my database like this:
private static void CreateSwaggerClient(DatabaseContext context)
{
var client = new Client
{
ClientId = "swaggerui",
ClientName = "Swagger UI client",
Flow = Flows.Implicit,
Enabled = true,
EnableLocalLogin = true,
AccessTokenType = AccessTokenType.Reference,
AllowAccessTokensViaBrowser = true,
IdentityTokenLifetime = 300,
AccessTokenLifetime = 3600,
AuthorizationCodeLifetime = 300,
AbsoluteRefreshTokenLifetime = 2592000,
SlidingRefreshTokenLifetime = 1296000,
RedirectUris = new List<ClientRedirectUri>
{
new ClientRedirectUri { Uri = "http://localhost:62668/swagger" }
},
AllowedScopes = new List<ClientScope>()
{
new ClientScope
{
Scope = "api"
}
},
ClientSecrets = new List<ClientSecret>()
{
new ClientSecret
{
Value = "secret".Sha256(),
Type = "SharedSecret"
}
}
};
context.Clients.Add(client);
context.SaveChanges();
}
Which has access to my api Scope:
private static void CreateScope(DatabaseContext context)
{
var scope = new Scope
{
Enabled = true,
Name = "api",
DisplayName = "Cormar API",
Description = "Should only be used for trusted internal service side applications",
Required = true,
Emphasize = true,
Type = (int)ScopeType.Resource,
IncludeAllClaimsForUser = false,
ShowInDiscoveryDocument = true,
AllowUnrestrictedIntrospection = true,
ScopeClaims = new List<ScopeClaim>()
{
new ScopeClaim
{
Name = "role",
Description = "Role claim types",
AlwaysIncludeInIdToken = true
},
new ScopeClaim
{
Name = "name",
Description = "The name of the user",
AlwaysIncludeInIdToken = true
},
new ScopeClaim
{
Name ="password",
Description = "Contains the encrypted password for a user",
AlwaysIncludeInIdToken = true
}
},
ScopeSecrets = new List<ScopeSecret>()
{
new ScopeSecret
{
Value = "anothersecret".Sha256(),
Type = "SharedSecret"
}
}
};
context.Scopes.Add(scope);
context.SaveChanges();
}
If I open a browser and navigate to the authorize url like this: https://localhost:44313/identity/connect/authorize?client_id=swaggerui&redirect_uri=http://localhost:62668/swagger&response_type=token&scope=api&state=moo it takes me to a login page, which when I type the username and password brings me to the swagger page with a access_token appended to the URL like this:
#access_token=b49fe5641519c325c17d248d2372d69f&token_type=Bearer&expires_in=3600&scope=api&state=moo
But the issue here is that if I click anything, the access token is removed from the url and if I try any of my endpoints, they all fail with access denied.
I have setup my swagger config like this:
private static void ConfigureSwagger(HttpConfiguration config)
{
config.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "test API");
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var commentsFileName = Assembly.GetExecutingAssembly().GetName().Name + ".XML";
var commentsFile = Path.Combine(baseDirectory, "bin", commentsFileName);
c.IncludeXmlComments(commentsFile);
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit")
.AuthorizationUrl("http://localhost:62668/identity/connect/authorize")
.TokenUrl("http://localhost:62668/identity/connect/token")
.Scopes(scopes =>
{
scopes.Add("api", "api access");
});
c.OperationFilter<AssignOAuth2SecurityRequirements>();
}).EnableSwaggerUi(c =>
{
c.EnableOAuth2Support("swaggerui", "secret", "local", "test");
});
}
Can anyone tell me what I am missing?
I managed to get this working.
First of all, my AssignOAuth2SecurityRequirements was setup incorrectly. I actually found the right code here: http://knowyourtoolset.com/2015/08/secure-web-apis-with-swagger-swashbuckle-and-oauth2-part-2/
public class AssignOAuth2SecurityRequirements: IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var actFilters = apiDescription.ActionDescriptor.GetFilterPipeline();
var allowsAnonymous = actFilters.Select(f => f.Instance).OfType<OverrideAuthorizationAttribute>().Any();
if (allowsAnonymous)
return; // must be an anonymous method
//var scopes = apiDescription.ActionDescriptor.GetFilterPipeline()
// .Select(filterInfo => filterInfo.Instance)
// .OfType<AllowAnonymousAttribute>()
// .SelectMany(attr => attr.Roles.Split(','))
// .Distinct();
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{"oauth2", new List<string> {"api"}}
};
operation.security.Add(oAuthRequirements);
}
}
Next, the redirect_uris for my client were incorrect. They all have to be https and they need the full redirect uri. Mine became this:
new ClientRedirectUri { Uri = "https://localhost:44313/swagger/ui/o2c-html" },
Once these were set up, it all started working.
I have an IdentityServer3 working with Windows Authentication Service. Now I want to handle the SAML2 protocol on my IdentityServer3 and I saw Kentor could do it for me.
The problem is that Kentor is using OpenID Connect in all samples, I searched a while but i couldn't find any documentation on how to combine Kentor with WindowsAuth. After many tries without success, I come here to ask if it is realy possible and how ?
Here is my (non-working) configuration in Startup.cs :
public void Configuration(IAppBuilder appBuilder)
{
appBuilder.Map("/windows", ConfigureWindowsTokenProvider);
appBuilder.UseIdentityServer(GetIdentityServerOptions());
}
private void ConfigureWsFederation(IAppBuilder pluginApp, IdentityServerOptions options)
{
var factory = new WsFederationServiceFactory(options.Factory);
factory.Register(new Registration<IEnumerable<RelyingParty>>(RelyingParties.Get()));
factory.RelyingPartyService = new Registration<IRelyingPartyService>(typeof(InMemoryRelyingPartyService));
factory.CustomClaimsService = new Registration<ICustomWsFederationClaimsService>(typeof(ClaimsService));
factory.CustomRequestValidator = new Registration<ICustomWsFederationRequestValidator>(typeof(RequestValidator));
var wsFedOptions = new WsFederationPluginOptions
{
IdentityServerOptions = options,
Factory = factory,
};
pluginApp.UseWsFederationPlugin(wsFedOptions);
}
private IdentityServerOptions GetIdentityServerOptions()
{
DefaultViewServiceOptions viewServiceOptions = new DefaultViewServiceOptions();
viewServiceOptions.CustomViewDirectory = HttpContext.Current.Server.MapPath("~/Templates");
viewServiceOptions.Stylesheets.Add("/Content/Custom.css");
IdentityServerServiceFactory factory = new IdentityServerServiceFactory()
.UseInMemoryClients(new List<Client>())
.UseInMemoryScopes(new List<Scope>());
factory.ConfigureDefaultViewService(viewServiceOptions);
factory.UserService = new Registration<IUserService>(resolver => new UserService());
return new IdentityServerOptions
{
SigningCertificate = Certificate.Load(),
Factory = factory,
PluginConfiguration = ConfigureWsFederation,
EventsOptions = new EventsOptions
{
RaiseSuccessEvents = true,
RaiseFailureEvents = true,
},
AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions
{
IdentityProviders = ConfigureIdentityProviders,
EnableLocalLogin = false,
},
RequireSsl = true,
};
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
ConfigureWSFederationProvider(app, signInAsType);
ConfigureKentorProvider(app, signInAsType);
}
private void ConfigureKentorProvider(IAppBuilder app, string signInAsType)
{
SPOptions spOptions = new SPOptions
{
EntityId = new EntityId("Dropbox"),
};
KentorAuthServicesAuthenticationOptions kentorOptions = new KentorAuthServicesAuthenticationOptions(false)
{
Caption = "Windows",
SignInAsAuthenticationType = signInAsType,
SPOptions = spOptions,
};
IdentityProvider idp = new IdentityProvider(new EntityId("http://stubidp.kentor.se/Metadata"), spOptions)
{
Binding = Saml2BindingType.HttpRedirect,
AllowUnsolicitedAuthnResponse = true,
LoadMetadata = true,
};
kentorOptions.IdentityProviders.Add(idp);
app.UseKentorAuthServicesAuthentication(kentorOptions);
}
private void ConfigureWSFederationProvider(IAppBuilder app, string signInAsType)
{
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
{
AuthenticationType = "windows",
Caption = "Windows",
SignInAsAuthenticationType = signInAsType,
MetadataAddress = serverHost + "windows",
Wtrealm = "urn:idsrv3",
});
}
private void ConfigureWindowsTokenProvider(IAppBuilder app)
{
app.UseWindowsAuthenticationService(new WindowsAuthenticationOptions
{
IdpReplyUrl = serverHost,
SigningCertificate = Certificate.Load(),
EnableOAuth2Endpoint = false,
});
}
This configuration builds, but when I use the Dropbox SSO (using SAML2) I get the exception No Idp with entity id "Dropbox" found.
You've configured "Dropbox" as the identity (EntityId in SAML2 terms) of your application (the one in SpOptions). That should be a URI that identifies your application. The convention is to use the URL to the metadata (~/AuthServices).
You need to add an IdentityProvider with the settings of the dropbox idp. Please also note that an EntityId of "Dropbox" won't work as the SAML2 standard requires an Entity ID to be an absolute URI.
I am using the Foundation SDK with C#, trying to launch a simple server in a minimal fashion.
Here is my attempt sofar.
public void StartServer()
{
var config = new ApplicationConfiguration
{
ApplicationName = "TestServer",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = #"Windows",
StorePath = #"CurrentUser\My",
SubjectName = Utils.Format(#"CN={0}, DC={1}", "TestServer", Dns.GetHostName())
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = #"Windows",
StorePath = #"CurrentUser\TrustedPeople",
},
NonceLength = 32,
AutoAcceptUntrustedCertificates = true,
ConfigureFirewall = false
},
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
ServerConfiguration = new ServerConfiguration
{
SecurityPolicies = new ServerSecurityPolicyCollection
{
new ServerSecurityPolicy
{
SecurityLevel = 0,
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#None"
}
},
UserTokenPolicies = new UserTokenPolicyCollection
{
new UserTokenPolicy { TokenType = UserTokenType.Anonymous }
},
DiagnosticsEnabled = true,
MaxSessionCount = 100,
MinSessionTimeout = 5000,
MaxSessionTimeout = 10000,
MaxBrowseContinuationPoints = 10,
MaxQueryContinuationPoints = 10,
MaxHistoryContinuationPoints = 100,
MaxRequestAge = 600000,
MinPublishingInterval = 100,
MaxPublishingInterval = 3600000,
PublishingResolution = 50,
MaxSubscriptionLifetime = 3600000,
MaxMessageQueueSize = 10,
MaxNotificationQueueSize = 100,
MaxNotificationsPerPublish = 1000,
MinMetadataSamplingInterval = 1000
}
};
config.Validate(ApplicationType.Server);
var server = new MyCustomServer();
server.Start(config);
}
When I try to call the method, I get the following exception:
Opc.Ua.ServiceResultException: Server does not have an instance certificate assigned.
à Opc.Ua.ServerBase.OnServerStarting(ApplicationConfiguration configuration) dans ...\OPC Foundation\Stack\Core\Stack\Server\ServerBase.cs:ligne 1607
à Opc.Ua.Server.StandardServer.OnServerStarting(ApplicationConfiguration configuration) dans ...\OPC Foundation\SampleApplications\SDK\Server\Server\StandardServer.cs:ligne 2628
à Opc.Ua.ServerBase.Start(ApplicationConfiguration configuration) dans ...\OPC Foundation\Stack\Core\Stack\Server\ServerBase.cs:ligne 232
à SlimFixtures.ServerDriver.StartServer() dans ...\ServerDriver.cs:ligne 71
What am I doing wrong?
So you found that servers based on foundation code always need a certificate.
Creating a self-signed certificate is easy, and does not need Admin login if you are using the Current User/My Windows store.
You can call this extension method after you validate:
config.Validate(ApplicationType.Server);
config.EnsureApplicationCertificate();
elsewhere
public static class ServiceExtensions
{
/// <summary>
/// Ensures the application certificate is present and valid.
/// </summary>
public static void EnsureApplicationCertificate(this ApplicationConfiguration configuration)
{
const ushort keySize = 1024;
const ushort lifetimeInMonths = 300;
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
bool errorFlag = false;
string hostName = Dns.GetHostName();
var serverDomainNames = configuration.GetServerDomainNames();
var applicationCertificate = configuration.SecurityConfiguration.ApplicationCertificate;
var certificate = applicationCertificate.Find(true);
if (certificate != null)
{
// if cert found then check domains
var domainsFromCertficate = Utils.GetDomainsFromCertficate(certificate);
foreach (string serverDomainName in serverDomainNames)
{
if (Utils.FindStringIgnoreCase(domainsFromCertficate, serverDomainName))
{
continue;
}
if (String.Equals(serverDomainName, "localhost", StringComparison.OrdinalIgnoreCase))
{
if (Utils.FindStringIgnoreCase(domainsFromCertficate, hostName))
{
continue;
}
var hostEntry = Dns.GetHostEntry(hostName);
if (hostEntry.Aliases.Any(alias => Utils.FindStringIgnoreCase(domainsFromCertficate, alias)))
{
continue;
}
if (hostEntry.AddressList.Any(ipAddress => Utils.FindStringIgnoreCase(domainsFromCertficate, ipAddress.ToString())))
{
continue;
}
}
Trace.TraceInformation("The application is configured to use domain '{0}' which does not appear in the certificate.", serverDomainName);
errorFlag = true;
} // end for
// if no errors and keySizes match
if (!errorFlag && (keySize == certificate.PublicKey.Key.KeySize))
{
return; // cert okay
}
}
// if we get here then we'll create a new cert
if (certificate == null)
{
certificate = applicationCertificate.Find(false);
if (certificate != null)
{
Trace.TraceInformation("Matching certificate with SubjectName '{0}' found but without a private key.", applicationCertificate.SubjectName);
}
}
// lets check if there is any to delete
if (certificate != null)
{
using (var store2 = applicationCertificate.OpenStore())
{
store2.Delete(certificate.Thumbprint);
}
}
if (serverDomainNames.Count == 0)
{
serverDomainNames.Add(hostName);
}
CertificateFactory.CreateCertificate(applicationCertificate.StoreType, applicationCertificate.StorePath, configuration.ApplicationUri, configuration.ApplicationName, null, serverDomainNames, keySize, lifetimeInMonths);
Trace.TraceInformation("Created new certificate with SubjectName '{0}', in certificate store '{1}'.", applicationCertificate.SubjectName, applicationCertificate.StorePath);
configuration.CertificateValidator.Update(configuration.SecurityConfiguration);
}
}
With a more recent version of the library, there is a built-in option to check the application instance certificate. It's available on the ApplicationInstance class.
Here's how you'd use it:
var applicationConfiguration = new ApplicationConfiguration
{
ApplicationName = "Aggregation server",
...
};
await applicationConfiguration.Validate(ApplicationType.ClientAndServer);
var applicationInstance = new ApplicationInstance(applicationConfiguration);
// This call will check that the application instance certificate exists, and will create it if not
var result =
await applicationInstance.CheckApplicationInstanceCertificate(false, CertificateFactory.DefaultKeySize);
var server = new AggregationServer();
await applicationInstance.Start(server);
System.Console.ReadKey();
server.Stop();
I'm trying to upgrade my MVC website to use the new OpenID Connect standard. The OWIN middleware seems to be pretty robust, but unfortunately only supports the
"form_post" response type. This means that Google isn't compatible, as it returns all the tokens in a the url after a "#", so they never reach the server and never trigger the middleware.
I've tried to trigger the response handlers in the middleware myself, but that doesn't seem to work at all, so I've got a simply javascript file that parses out the returned claims and POSTs them to a controller action for processing.
Problem is, even when I get them on the server side I can't parse them correctly. The error I get looks like this:
IDX10500: Signature validation failed. Unable to resolve
SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
IsReadOnly = False,
Count = 1,
Clause[0] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
),
token: '{
"alg":"RS256",
"kid":"073a3204ec09d050f5fd26460d7ddaf4b4ec7561"
}.
{
"iss":"accounts.google.com",
"sub":"100330116539301590598",
"azp":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com",
"nonce":"7c8c3656118e4273a397c7d58e108eb1",
"email_verified":true,
"aud":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com",
"iat":1429556543,"exp\":1429560143
}'."
}
My token verification code follows the example outlined by the good people developing IdentityServer
private async Task<IEnumerable<Claim>> ValidateIdentityTokenAsync(string idToken, string state)
{
// New Stuff
var token = new JwtSecurityToken(idToken);
var jwtHandler = new JwtSecurityTokenHandler();
byte[][] certBytes = getGoogleCertBytes();
for (int i = 0; i < certBytes.Length; i++)
{
var certificate = new X509Certificate2(certBytes[i]);
var certToken = new X509SecurityToken(certificate);
// Set up token validation
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = googleClientId;
tokenValidationParameters.IssuerSigningToken = certToken;
tokenValidationParameters.ValidIssuer = "accounts.google.com";
try
{
// Validate
SecurityToken jwt;
var claimsPrincipal = jwtHandler.ValidateToken(idToken, tokenValidationParameters, out jwt);
if (claimsPrincipal != null)
{
// Valid
idTokenStatus = "Valid";
}
}
catch (Exception e)
{
if (idTokenStatus != "Valid")
{
// Invalid?
}
}
}
return token.Claims;
}
private byte[][] getGoogleCertBytes()
{
// The request will be made to the authentication server.
WebRequest request = WebRequest.Create(
"https://www.googleapis.com/oauth2/v1/certs"
);
StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream());
string responseFromServer = reader.ReadToEnd();
String[] split = responseFromServer.Split(':');
// There are two certificates returned from Google
byte[][] certBytes = new byte[2][];
int index = 0;
UTF8Encoding utf8 = new UTF8Encoding();
for (int i = 0; i < split.Length; i++)
{
if (split[i].IndexOf(beginCert) > 0)
{
int startSub = split[i].IndexOf(beginCert);
int endSub = split[i].IndexOf(endCert) + endCert.Length;
certBytes[index] = utf8.GetBytes(split[i].Substring(startSub, endSub).Replace("\\n", "\n"));
index++;
}
}
return certBytes;
}
I know that Signature validation isn't completely necessary for JWTs but I haven't the slightest idea how to turn it off. Any ideas?
I thought I'd post my slightly improved version which uses JSON.Net to parse Googles' X509 Certificates and matches the key to use based on the "kid" (key-id). This is a bit more efficient than trying each certificate, since asymmetric crypto is usually quite expensive.
Also removed out-dated WebClient and manual string parsing code:
static Lazy<Dictionary<string, X509Certificate2>> Certificates = new Lazy<Dictionary<string, X509Certificate2>>( FetchGoogleCertificates );
static Dictionary<string, X509Certificate2> FetchGoogleCertificates()
{
using (var http = new HttpClient())
{
var json = http.GetStringAsync( "https://www.googleapis.com/oauth2/v1/certs" ).Result;
var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>( json );
return dictionary.ToDictionary( x => x.Key, x => new X509Certificate2( Encoding.UTF8.GetBytes( x.Value ) ) );
}
}
JwtSecurityToken ValidateIdentityToken( string idToken )
{
var token = new JwtSecurityToken( idToken );
var jwtHandler = new JwtSecurityTokenHandler();
var certificates = Certificates.Value;
try
{
// Set up token validation
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = _clientId;
tokenValidationParameters.ValidIssuer = "accounts.google.com";
tokenValidationParameters.IssuerSigningTokens = certificates.Values.Select( x => new X509SecurityToken( x ) );
tokenValidationParameters.IssuerSigningKeys = certificates.Values.Select( x => new X509SecurityKey( x ) );
tokenValidationParameters.IssuerSigningKeyResolver = ( s, securityToken, identifier, parameters ) =>
{
return identifier.Select( x =>
{
if (!certificates.ContainsKey( x.Id ))
return null;
return new X509SecurityKey( certificates[ x.Id ] );
} ).First( x => x != null );
};
SecurityToken jwt;
var claimsPrincipal = jwtHandler.ValidateToken( idToken, tokenValidationParameters, out jwt );
return (JwtSecurityToken)jwt;
}
catch (Exception ex)
{
_trace.Error( typeof( GoogleOAuth2OpenIdHybridClient ).Name, ex );
return null;
}
}
The problem is the kid in the JWT whose value is the key identifier of the key was used to sign the JWT. Since you construct an array of certificates manually from the JWKs URI, you lose the key identifier information. The validation procedure however requires it.
You'll need to set tokenValidationParameters.IssuerSigningKeyResolver to a function that will return the same key that you set above in tokenValidationParameters.IssuerSigningToken. The purpose of this delegate is to instruct the runtime to ignore any 'matching' semantics and just try the key.
See this article for more information: JwtSecurityTokenHandler 4.0.0 Breaking Changes?
Edit: the code:
tokenValidationParameters.IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { return new X509SecurityKey(certificate); };
The folks at Microsoft posted code sample for Azure V2 B2C Preview endpoint that support OpenId Connect. See here, with the helper class OpenIdConnectionCachingSecurityTokenProvider the code is simplified as follows:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(new TokenValidationParameters
{
ValidAudiences = new[] { googleClientId },
}, new OpenIdConnectCachingSecurityTokenProvider("https://accounts.google.com/.well-known/openid-configuration"))});
This class is necessary because the OAuthBearer Middleware does not leverage. The OpenID Connect metadata endpoint exposed by the STS by default.
public class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityTokenProvider
{
public ConfigurationManager<OpenIdConnectConfiguration> _configManager;
private string _issuer;
private IEnumerable<SecurityToken> _tokens;
private readonly string _metadataEndpoint;
private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();
public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)
{
_metadataEndpoint = metadataEndpoint;
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);
RetrieveMetadata();
}
/// <summary>
/// Gets the issuer the credentials are for.
/// </summary>
/// <value>
/// The issuer the credentials are for.
/// </value>
public string Issuer
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _issuer;
}
finally
{
_synclock.ExitReadLock();
}
}
}
/// <summary>
/// Gets all known security tokens.
/// </summary>
/// <value>
/// All known security tokens.
/// </value>
public IEnumerable<SecurityToken> SecurityTokens
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _tokens;
}
finally
{
_synclock.ExitReadLock();
}
}
}
private void RetrieveMetadata()
{
_synclock.EnterWriteLock();
try
{
OpenIdConnectConfiguration config = _configManager.GetConfigurationAsync().Result;
_issuer = config.Issuer;
_tokens = config.SigningTokens;
}
finally
{
_synclock.ExitWriteLock();
}
}
}
Based on the answer from Johannes Rudolph I post my solution.
There is a compiler error in IssuerSigningKeyResolver Delegate which I had to solve.
This is my working code now:
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace QuapiNet.Service
{
public class JwtTokenValidation
{
public async Task<Dictionary<string, X509Certificate2>> FetchGoogleCertificates()
{
using (var http = new HttpClient())
{
var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs");
var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>();
return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value)));
}
}
private string CLIENT_ID = "xxxxx.apps.googleusercontent.com";
public async Task<ClaimsPrincipal> ValidateToken(string idToken)
{
var certificates = await this.FetchGoogleCertificates();
TokenValidationParameters tvp = new TokenValidationParameters()
{
ValidateActor = false, // check the profile ID
ValidateAudience = true, // check the client ID
ValidAudience = CLIENT_ID,
ValidateIssuer = true, // check token came from Google
ValidIssuers = new List<string> { "accounts.google.com", "https://accounts.google.com" },
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
{
return certificates
.Where(x => x.Key.ToUpper() == kid.ToUpper())
.Select(x => new X509SecurityKey(x.Value));
},
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromHours(13)
};
JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
SecurityToken validatedToken;
ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);
return cp;
}
}
}