ADFS RefreshToken rotation with MSAL - jwt

Environment
.NetCore 3.1 Client
ADFS 2019
MSAL via Microsoft.Identity.Client, Version=4.18.0.0
I'm using MSAL to authenticate directly to a customer ADFS 2019 instance using the ROPC flow (username/password) for a .NetCore desktop client. I'm able to get an IDToken & RefreshToken at initial login, and then MSAL (which keeps the RefreshToken privately under wraps) will acquire new IDTokens for a period of time until the RefreshToken expires.
My question is, is there a way for me to get ADFS to issue a new RefreshToken or do RefreshToken rotation while I continue to want to be signed in? Currently, once the default 480 minutes passes - during which new IDTokens are issued correctly - the call to .AcquireTokenSilent() fails with MsalUIRequiredException "The refresh token in parameter 'refresh_token' has expired".
Microsoft says here that adding the 'aza' scope will force a RefreshToken update (if using OAuth2.0 extensions ?), but that's not what I see (hence why it's commented). When I add the 'aza' scope, I do get a new IDToken on every request instead of it hitting the local account cache, but no new RefreshTokens come back. I'm verifying this with ngrok looking at the traffic to my ADFS instance.
static void Main(string[] args)
{
app = PublicClientApplicationBuilder.Create("CLIENT_ID")
//.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
.WithAdfsAuthority("ADFS_URI", false)
//.WithExtraQueryParameters(new Dictionary<string, string> { {"forceRefresh", "true" } })
.Build();
while (true)
{
var result = GetToken().GetAwaiter().GetResult();
Console.WriteLine($"IdTok: {ComputeSha256Hash(result.IdToken)}, Exp: {result.ExpiresOn}");
Thread.Sleep(1000 * 60 * 2);
}
}
private static async Task<AuthenticationResult> GetToken()
{
var accounts = app.GetAccountsAsync().GetAwaiter().GetResult();
if (accounts == null || accounts.Count() == 0)
{
var result = app
.AcquireTokenByUsernamePassword(new string[] { "email", "openid"/*, "aza"*/, "allatclaims", "profile" }, "TESTUSER", ConvertToSecureString("TESTPASS"))
.ExecuteAsync().GetAwaiter().GetResult();
accounts = app.GetAccountsAsync().GetAwaiter().GetResult();
}
var cachedResult = app.AcquireTokenSilent(new string[] { "email", "openid"/*, "aza"*/, "allatclaims", "profile" }, accounts.FirstOrDefault()).ExecuteAsync().GetAwaiter().GetResult();
return cachedResult;
}
Initial Login response Includes IDToken & RefreshToken
Subsequent Responses only include new IDToken
Until the RefreshToken is Rejected

Related

How can I get "Amazon.Extensions.CognitoAuthentication.CognitoUserSession.IDToken" From AWSCredentials?

I want get "Amazon.Extensions.CognitoAuthentication.CognitoUserSession.IDToken" From AWSCredentials.
I have AWSCredentials From Oauth Google Login.
public AWSCredentials GetAWSCredentials_Google(string token)
{
CognitoAWSCredentials credentials = new CognitoAWSCredentials(FED_POOL_ID, regionTable[REGION]);
credentials.AddLogin("accounts.google.com", token);
return credentials;
}
And, I use EC2 Instance and my ubuntu server is in there. Also, I was originally using a method of accessing the server by receiving a membership from Cognito User Pool, so I was using the following code.
private IEnumerator sendPostUser()
{
string uri = rootUrl + "/user";
string json = "{ ... }";
byte[] jsonData = System.Text.Encoding.UTF8.GetBytes(json);
using (UnityWebRequest request = UnityWebRequest.Post(uri, json))
{
if (request.uploadHandler != null)
request.uploadHandler.Dispose();
request.disposeUploadHandlerOnDispose = true;
request.disposeDownloadHandlerOnDispose = true;
request.uploadHandler = new UploadHandlerRaw(jsonData);
/* Header */
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("token", cloud_acess.GetComponent<ControlCloud>().cognitoUser.SessionTokens.IdToken);
/* Send Message */
yield return request.SendWebRequest();
...
}
By the way, there was a problem with this code "request.SetRequestHeader("token", cloud_acess.GetComponent().cognitoUser.SessionTokens.IdToken);".
This cognitouser means Amazon.Extensions.CognitoAuthentication.CognitoUser.
My Project get CognitoUser using user's ID and PW, and get AWSCredentials using this Cognitouser. But Google Login doesn't this process and just get credentials.
So, I can't get "cognitoUser.SessionTokens.IdToken". It makes me cannot to request anything from ec2 server.
How Can i get this? What should I do if the premise of this problem itself is wrong?
I tried to put all the tokens I received when I logged in to Google and the tokens I received as credentials in the header.But I failed.

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

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.

Azure Mobile Services backend serviceUser does not return Facebook identities as expected

I'm struggling with a Xamarin Forms (iOS)/Azure Mobile Services/Facebook issue that I don't know how to resolve. What I'm trying to do is login to Facebook using AMS and then save that user's details to a service side database via a custom controller. I am running Azure Mobile Services on the backend.
What I have in place is the code below that successfully logs in a Facebook user.
var fbUser = await DependencyService.Get<IMobileClient>().LoginAsync(MobileServiceAuthenticationProvider.Facebook);
I then want to save fbUser to the database where I'm using ASP.NET Identity tables all configured. I want to use this user to gain access to the facebook user's profile information. I therefore have a backend service custom controller action that looks like this:
[Route("logintofacebook")]
[AuthorizeLevel(AuthorizationLevel.Anonymous)]
public async Task<IHttpActionResult> LoginToFacebook(MobileServiceUser msUser)
{
try
{
if (msUser != null)
{
var serviceUser = User as ServiceUser;
var identities = await serviceUser.GetIdentitiesAsync();
var result = new JObject();
var fb = identities.OfType<FacebookCredentials>().FirstOrDefault();
if (fb == null)
{
return NotFound();
}
var googleCredentials = identities.OfType<GoogleCredentials>().FirstOrDefault();
var azure = identities.OfType<MicrosoftAccountCredentials>().FirstOrDefault();
var accessToken = fb.AccessToken;
result.Add("facebook",
await GetProviderInfo("https://graph.facebook.com/me?access_token=" + accessToken));
var email = GetUserInfo(result);
var userTypeId = UTypes.Facebook;
When debugging on the backend side, the MobileServiceUser is a valid object and I can check the token and Facebook userid which are the same as were created on the client. However, the highlighted line returns zero identities. This means that fb variable end up being null.
The question is, why are no identities being returned from the serviceUser variable above?
Here's what the debugged AMS token looks like when debugged with jwt.io
{
"iss": "urn:microsoft:windows-azure:zumo",
"aud": "urn:microsoft:windows-azure:zumo",
"nbf": 1446872000,
"exp": 1449464000,
"urn:microsoft:credentials": "{\"accessToken\":\"CAAL2gwRM4RYBAKV9Wp0Evjp2aATnm5OIHHc15ujJfeevqCW6DoI36HOQCOYq96xUjZA6VXwovnkBOlY0SkC9nrdwr8jdbF3qJdtK4GAHVk9SGxKVYUZBJ4UwPqQmb5yka93GzL0Fl86m93LnqTffIPJ6vkMfpP0ZAroKzmcJxM1pJ7BAAAA\"}",
"uid": "Facebook:0000000000000000",
"ver": "2"
}
(I've replaced out the facebook section with zeros)
thanks
O

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