OAuth security for website with API and only external providers - rest

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
};
});

Related

IdentityServer3 No scopeclaims when using reference token

In IdentityServer I've added a new scope like this:
new Scope
{
Name = "myscope",
Description = "myscope",
Type=ScopeType.Resource,
ShowInDiscoveryDocument= false,
Emphasize = false,
//AccessTokenType=1, //Reference
AccessTokenType=0, //JWT
Claims = new List<ScopeClaim>
{
new ScopeClaim("location"),
}
I've added a client:
new Client
{
ClientName = "myclient",
Enabled = true,
ClientId = "myclient",
Flow = Flows.Implicit,
AllowedScopes = new List<string> {"myscope"},
Claims = new List<Claim> {new Claim("location", "datacenter")}
}
I've added an implementation of GetProfileData :
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
await base.GetProfileDataAsync(context);
if (context.AllClaimsRequested)
context.IssuedClaims = context.Subject.Claims;
else if (context.RequestedClaimTypes != null)
context.IssuedClaims = context.Subject.Claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
}
In my webapi, I'm using AccessTokenValidation:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5300",
AllowedScopes = { "myscope" },
RequireHttpsMetadata = false,
});
services.AddAuthorization(options =>
{
options.AddPolicy("location", policy => policy.RequireClaim("location"));
});
My controller is prefixed with:
[Authorize(ActiveAuthenticationSchemes = "Bearer", Policy = "location")]
public async Task<IActionResult> Get()
{
...
}
Now, when the accesstoken is set to JWT, this works fine, I'm able to call the endpoint. Now, if I change AccessTokenType to reference token, it fails...
If I inspect the RequestedClaimTypes during the call to the profiledata endpoint, it holds the claims for 'myscope' when using JWT, but not when using Reference Token...
Am I missing some configuration or is this the way it's supposed work?? I would have expected to get the same claims in both setup
Reference Tokens are not self-contained tokens like JWTs are. They provide an ID that can be used to fetch the information that the reference token represents from a backing store.
If you're using IdentityServer3 out of the box, you should be able to request your reference token from the POST /connect/token endpoint and follow that up with a request to the token introspection endpoint:
POST /connect/accesstokenvalidation
token={tokenReceivedFromPreviousRequest}
This will return the information for that reference token that is kept in its backing store, including scopes.
As a note, that introspection endpoint accepts both Reference Tokens and JWTs.

IdentityServer3 Guidance Needed

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.

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();
}
}

Reusing ClaimsPrincipal to authenticate against sharepoint online

I have an Office 365 account (using the latest SharePoint 2013 instance)
I also have a simple .net web app that is authenticating against Office 365, I created an AppPrincipalId and added it using New-MsolServicePrincipal powershell commmand.
This works correctly. I launch the app (in debug), it redirects to 365 login, I login, it comes back to the app, and I have derived a class from ClaimsAuthenticationManager and overriden the Authenticate method.
I can now see the ClaimsPrincipal, with the relevant claims and identity etc.
Now I would like to re-use this identity to programmatically access SharePoint.
My questions:
a) Will SharePoint permit this Identity (seeing that it was issued by sts.windows.net)
b) How can I reconstruct a valid JWT (or use the existing one), and encapsulate this in a HttpRequest using authentication bearer.
The code I am using is below - this is coming back 401 not authorized.
Any help would be highly appreciated.
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated == true)
{
List<Claim> claims = null;
claims = (from item in incomingPrincipal.Claims
where item.Type.StartsWith("http", StringComparison.InvariantCultureIgnoreCase)
select item).ToList();
RNGCryptoServiceProvider cryptoProvider = new RNGCryptoServiceProvider();
byte[] keyForHmacSha256 = Convert.FromBase64String("Gs8Qc/mAF5seXcGHCUY/kUNELTE=");
// Create our JWT from the session security token
JWTSecurityToken jwt = new JWTSecurityToken
(
"https://sts.windows.net/myAppIdGuid/",
"00000003-0000-0ff1-ce00-000000000000", // sharepoint id
claims,
new SigningCredentials(
new InMemorySymmetricSecurityKey(keyForHmacSha256),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256"),
DateTime.UtcNow,
DateTime.UtcNow.AddHours(1)
);
var validationParameters = new TokenValidationParameters()
{
AllowedAudience = "00000003-0000-0ff1-ce00-000000000000", // sharepoint id
ValidIssuer = "https://sts.windows.net/myAppIdGuid/", // d3cbe is my app
ValidateExpiration = true,
ValidateNotBefore = true,
ValidateIssuer = true,
ValidateSignature = true,
SigningToken = new BinarySecretSecurityToken(Convert.FromBase64String("mySecretKeyFromPowerShellCommand")),
};
JWTSecurityTokenHandler jwtHandler = new JWTSecurityTokenHandler();
var jwtOnWire = jwtHandler.WriteToken(jwt);
var claimPrincipal = jwtHandler.ValidateToken(jwtOnWire, validationParameters);
JWTSecurityToken parsedJwt = jwtHandler.ReadToken(jwtOnWire) as JWTSecurityToken;
HttpWebRequest endpointRequest =
(HttpWebRequest)HttpWebRequest.Create(
"https://MySharepointOnlineUrl/_api/web/lists");
endpointRequest.Method = "GET";
endpointRequest.Accept = "application/json;odata=verbose";
endpointRequest.Headers.Add("Authorization",
"Bearer " + parsedJwt.RawData);
HttpWebResponse endpointResponse =
(HttpWebResponse)endpointRequest.GetResponse();
}
}
If your scenario is about consuming SharePoint Online data from a remote web app, you probably want to use the OAuth flow. You can't generate the token yourself. Instead you ask for consent to the user to access certain scopes (resource + permission). These two links should help
http://msdn.microsoft.com/en-us/library/office/apps/jj687470(v=office.15).aspx
http://jomit.blogspot.com.ar/2013/03/authentication-and-authorization-with.html