IdentityServer3 Guidance Needed - identityserver3

I have a scenario where a client has an OpenIdConnect (OIDC) token in their possession. The OIDC was issued from an external OIDC provider, I am not the OIDC provider, just the downstream consumer of it.
The goal is for the client to exchange said OIDC Token, for temporary credentials, or an accesstoken, which will then give them api access to more specific resources.
In my case, the OIDC represents a user. The client, has a ClientId/Secret, which is used to establish service-2-service trust. In the end I would like to have something that looks a lot like the CustomGrant token Request.
static TokenResponse GetCustomGrantToken()
{
var client = new TokenClient(
token_endpoint,
"custom_grant_client",
"cd19ac6f-3bfa-4577-9579-da32fd15788a");
var customParams = new Dictionary<string, string>
{
{ "some_custom_parameter", "some_value" }
};
var result = client.RequestCustomGrantAsync("custom", "read", customParams).Result;
return result;
}
where my customParams would contain the OIDC to my user.
Problem: I can get a token back from the GetCustomGrantToken call, however a follow up Webapi call fails to pass Authorization. i.e. Identity.isAuthenticated is false.
The it all works fine if I get a clientcredential token.
static TokenResponse GetClientToken()
{
var client = new TokenClient(
token_endpoint,
"silicon",
"F621F470-9731-4A25-80EF-67A6F7C5F4B8");
return client.RequestClientCredentialsAsync("api1").Result;
}
Had the CustomGrantToken worked I would have put my users account info in the claims, thus giving me context in the subsequent WebApi calls.
Any direction would be appreciated.

Related

OAuth security for website with API and only external providers

I have a .Net core 3.2 site with a RESTful API on one server, and a client website on another server. Users authenticate to the client app via only external providers such as Facebook, Google, or Microsoft. We also have an Identity Server 4.0 that we will be using, but will act just like another external provider.
The issue is that once the user is authenticated on the client, and their granted roles/claims have been determined, how do we request a particular resource from the API? The client web app knows about the user and knows the user is who they say they are, and the client app knows what they can do. How do we relay that information securely to the API?
I was considering client_credentials between the API and the web site, but it seems that is for situations where there is no user, like services or daemons.
I don't want the API to know or care about the users, just that they are authenticated and what their claims are.
To implement authentication in a single-page application, you need to use Authorization Code with PKCE OAuth2 flow. It lets you not store any secrets in your SPA.
Please don't use Implicit flow as it's deprecated because of security reasons.
When you send your token from a client to a properly configured .NET Core API, you should be able to read the User property of the controller for the identity information.
If you configure the API properly, a request will reach a controller only in case if an access token is valid.
The answer I was looking for was JWT Tokens:
On the client, before it sends the bearer token:
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var accessToken = await GetAccessTokenAsync();
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.SetBearerToken(accessToken);
}
return await base.SendAsync(request, cancellationToken);
}
public async Task<string> GetAccessTokenAsync()
{
var longKey = "FA485BA5-76C3-4FF5-8A33-E3693CA97002";
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(longKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim> {
new Claim("sub", _httpContextAccessor.HttpContext.User.GetUserId())
};
claims.AddRange(_httpContextAccessor.HttpContext.User.Claims);
var token =new JwtSecurityToken(
issuer: "https://localhost:44389",
audience: "https://localhost:44366",
claims: claims.ToArray(),
expires: DateTime.Now.AddMinutes(30),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
And on the API server
var longKey = "FA485BA5-76C3-4FF5-8A33-E3693CA97002";
services.AddAuthentication(x=> {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
//ValidateLifetime = true,
ValidateIssuerSigningKey = true,
//ValidIssuer = "https://localhost:44366",
//ValidAudience = "https://localhost:44366",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(longKey)),
//ClockSkew = TimeSpan.Zero
};
});

.Net Core 3.1 SignalR Client - How to add the JWT Token string to SignalR connection configuration?

I am using the SignalR .net core client in my project with JWT Tokens.
In the sample code below, the string variable "tokenString" has already been configured as an actual token and therefore i don't need to call upon an external method to create the token, that part has already been done before I reach this method. Using debug, and also testing the "tokenString" value on JWT website, I know the token is working, its just the fact I dont know how to use the ready made token in the SignalR connection method.
How do I configure the SignalR client connection to use this tokenString?
localConConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:44372/LocalConnectorHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenString); // Not working
// Need a solution like this: options.Token = tokenString
})
.WithAutomaticReconnect()
.Build();
The issue was the fact that the [Authorize] attribute I had configured in the SignalR Hub class needed to define the authentication scheme to use, [Authorize] attribute alone was not enough.
SignalR Hub Class:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class LocalConnectorHub : Hub
{
public async Task SendToMacros(string serverName, string data)
{
await Clients.All.SendAsync("MacrosInbound", serverName, data);
}
public async Task ConnectorStatus(string serverName, string data)
{
await Clients.All.SendAsync("UpdateConnectorStatus", serverName, data);
}
}
SignalR .NET Core Client Connection:
localConConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:44372/LocalConnectorHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenString);
})
.WithAutomaticReconnect()
.Build();
await localConConnection.StartAsync();
Further example code from the startup.cs class (inside configure services method), this is posted to help one of our fellow members in the comments below:
// Retrieve the secret key from the appsettings.json file used for encryption
// when generating the JWT token for REST API authentication.
var key = Encoding.ASCII.GetBytes(Configuration.GetSection("AppSettings:Token").Value);
// Added to original .net core template.
// The preceding code configures multiple authentication methods. The app uses cookie-based authentication to log in
// via the browser using the identity manager. The second methid uses JWT bearer authentication for the REST API.
// The preceding cookie configuration configures Identity with default option values.
// Services are made available to the app through dependency injection.
// Cookie configuration MUST be called after calling AddIdentity or AddDefaultIdentity.
// IMPORTANT NOTE:
// When we decorate controllers or classes with use the [Authorize] attribute, it actually binds to the first authentication
// system by default (in this case cookie authentication) The trick is to change the attribute to specify which authorization
// service we want to use. Anexample for a protected respurce for a REST API controller would be to decorate using:
// "[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]"
services.AddAuthentication()
.AddCookie(options =>
{
// Cookie settings
options.Cookie.Name = "MyCompanyName";
// HttpOnly is a flag that can be used when setting a cookie to block access to the cookie from client side scripts.
// Javascript for example cannot read a cookie that has HttpOnly set. This helps mitigate a large part of XSS attacks
// as many of these attempt to read cookies and send them back to the attacker, possibly leaking sensitive information
// or worst case scenario, allowing the attacker to impersonate the user with login cookies.
options.Cookie.HttpOnly = true;
// CookieAuthenticationOptions.ExpireTimespan is the option that allows you to set how long the issued cookie is valid for.
// The cookie is valid for (XX) minutes from the time of creation. Once those XX minutes are up the user will have to sign
// back in becuase if the SlidingExpiration is set to false.
// If SlidingExpiration is set to true then the cookie would be re-issued on any request half way through the ExpireTimeSpan.
// For example, if the user logged in and then made a second request half way through the permitted timespan then the cookie
// would be re-issued for another (XX) minutes. If the user logged in and then made a second request AFTER (XX) minutes later
// then the user would be prompted to log in.
// You can also change the units i.e. TimeSpan.FromHours(10); OR TimeSpan.FromDays(10);
// In a nutshell, setting the options.ExpireTimeSpan is equivalent to setting an idle time out period...
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
// Sliding expiration resets the expiration time for a valid authentication cookie if a request is made and more than half of the
// timeout interval has elapsed.If the cookie expires, the user must re - authenticate.Setting the SlidingExpiration property to
// false can improve the security of an application by limiting the time for which an authentication cookie is valid, based on the
// configured timeout value.
options.SlidingExpiration = true;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this
// claim is generally application specific. The "iss" value is a case-sensitive string containing
// a StringOrURI value. Use of this claim is OPTIONAL.
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
// The "iss" (issuer) claim identifies the principal that issued the JWT.The processing of this
// claim is generally application specific. The "iss" value is a case-sensitive string containing
// a StringOrURI value.Use of this claim is OPTIONAL.
ValidateIssuer = false,
// Usually, this is your application base URL
ValidIssuer = "http://localhost:45092/",
// The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal
// intended to process the JWT MUST identify itself with a value in the audience claim. If the principal
// processing the claim does not identify itself with a value in the "aud" claim when this claim is present,
// then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings,
// each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value
// MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience
// values is generally application specific. Use of this claim is OPTIONAL.
ValidateAudience = false,
//Here, we are creating and using JWT within the same application.
//In this case, base URL is fine.
//If the JWT is created using a web service, then this would be the consumer URL.
ValidAudience = "http://localhost:45092/",
// The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted
// for processing. The processing of the "exp" claim requires that the current date/time MUST be before the
// expiration date/time listed in the "exp" claim.
RequireExpirationTime = true,
// Check if token is not expired and the signing key of the issuer is valid (ValidateLifetime = true)
ValidateLifetime = true,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Appsettings.json file (dont store keys here for production :)
"AppSettings": {
"Token": "secret key for jwt"
}

Servicestack Session is null only when using JWT

This fails on SessionAs in the baseservice in Postman when I authenticate via JWT. But when I use Basic Auth it works fine. Anyone know why?
Apphost
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[]
{
new BasicAuthProvider(), //Sign-in with HTTP Basic Auth
new JwtAuthProvider(AppSettings) {
AuthKeyBase64 = AppSettings.GetString("jwt.auth.key"),
RequireSecureConnection = false,
}, //JWT TOKENS
new CredentialsAuthProvider(AppSettings)
})
{
BaseService
public class ServiceBase: Service
{
public IUserAuth UserAuth
{
get
{
var session = SessionAs<AuthUserSession>();
return AuthRepository.GetUserAuth(session.UserAuthId);
}
}
}
Your SessionAs<T> needs to match the UserSession Type registered in the AuthFeature plugin which is CustomUserSession.
ServiceStack's JwtAuthProvider populates the UserAuthId in the JWT's sub JWT Payload so you should check the Raw HTTP Headers to make sure the JWT Token is being sent, either in HTTP's Authorization Header as a BearerToken or in the ss-tok Cookie. If it is being sent you decode the JWT sent in https://jwt.io to make sure it contains a valid payload, in this case it contains a "sub" property in the JWT payload containing the UserAuthId of the user being authenticated.

ADAL - ClientAssertionCertificate

We can successfully acquire a token using the following code:
var certificate = Certificate.Load("Client.pfx", "notasecret");
var authenticationContext = new AuthenticationContext(authority);
var clientAssertionCertificate = new ClientAssertionCertificate(clientId, certificate);
return await authenticationContext.AcquireTokenAsync(resource, clientAssertionCertificate);
The token doesnt seem to contain any information that we can use to identity the client. In our use case we have lots of daemon service clients that communicate to a API. We need to have some unique identified available on the server.
I also tried creating our own JWT token and added some public claims, such as name. However after requesting client assertion type using the following code fragment
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "clientid", clientId },
{ "resource", resource },
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
{ "grant_type", "client_credentials" },
{ "client_assertion", jwt }
});
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://login.windows.net/{guid}/")
};
var response = await httpClient.PostAsync("oauth2/token", content);
The return token had none of my custom information.
Question: Is there a way to pass custom claims using ClientAssertionCertificate flow? where the token returned has additional information.
There is currently no way of adding custom claims in tokens issued for applications.
The token you receive should contain the claims appid (which identifies the client_id of the application who requested the token) and tid (which indicates which azure AD tenant the app is operating on). Those two should be enough for you to identify the calling application. Now, if rather than the application you want to identify the process (as in, instance of application X running on server A and instance of application X running on server B) then I don't believe we have anything in Azure AD today that would help you to tell the two apart - for Azure AD if they have the same client_id and secret, they are the same application.

Pass a ADFS token to a custom STS service

I am testing a product that authenticates uses using a custom STS service. The way it used to work is, when a user hits the website using the browser, we issue a redirect to hit the STS service. the STS service authenticates the user by hitting AD and then issues a SAML token with some custom claims for the user. The website then hits the STS once again to get a ActAs token so we can communicate with the data service.
And I had a automation that would mimic this behavior and its working fine in production.
We are not modifying the STS to use ADFS to authenticate instead of hitting the AD directly. So now when I hit the website, the request gets redirected to a ADFS endpoint which authenticates the user and issues a token. Then we hit the custom STS service that would use the token to authenticate the user (instead of hitting AD), add custom claims and issue a SAML token for the user. We then generate a ActAs token using this to finally hit the data service.
I am trying to update my automation for this changed behavior. So what I am doing now is hit the ADFS service, obtain a token and pass the token to the STS service so it can issue me a SAML token.
I am quite an amateur when it comes to windows identity service so i am having hard time trying to get this work. I have successfully obtained the token (Bearer Token) from the ADFS but i cant figureout how to pass this token to my custom STS so it can issue me a SAML token.
Any help would be highly appreciated. Thanks!
here is the code i am using
public static SecurityToken GetSecurityToken()
{
var endPoint = new EndpointAddress(new Uri(#"ADFS endpoint"));
var msgBinding = new WS2007HttpBinding(SecurityMode.TransportWithMessageCredential, false);
msgBinding.Security.Message.EstablishSecurityContext = false;
msgBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
var factory = new WSTrustChannelFactory(msgBinding, endPoint);
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.SupportInteractive = true;
factory.Credentials.UserName.UserName = "user";
factory.Credentials.UserName.Password = "pwd";
var rst = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
AppliesTo = new EndpointReference(#"custom STS endpoint")
};
return factory.CreateChannel().Issue(rst);
}
public static void GetUserClaimsFromSecurityTokenService(SecurityToken secToken)
{
var securityTokenManager = new SecurityTokenHandlerCollectionManager(string.Empty);
securityTokenManager[string.Empty] = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
var trustChannelFactory = new WSTrustChannelFactory(Binding, new EndpointAddress("custom STS endpoint"))
{
TrustVersion = TrustVersion.WSTrust13,
SecurityTokenHandlerCollectionManager = securityTokenManager,
};
var rst = new RequestSecurityToken(RequestTypes.Issue)
{
AppliesTo = new EndpointReference("website url"),
TokenType = SamlSecurityTokenHandler.Assertion
};
var channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
channel.Open(TimeSpan.FromMinutes(15));
try
{
RequestSecurityTokenResponse rstr;
SecurityToken token = channel.Issue(rst, out rstr);
var genericToken = (GenericXmlSecurityToken)token;
var req = new SamlSecurityTokenRequirement();
var handler = new SamlSecurityTokenHandler(req)
{
Configuration = new SecurityTokenHandlerConfiguration()
};
var newToken = handler.ReadToken(new XmlNodeReader(genericToken.TokenXml));
}
finally
{
channel.Close();
}
}