The authentication part was done using Identity Server 4, now I want to change the clientId and clientSecret, and use the new credentials, but in the backend I can't find the client configuration and I don't know how it's configured. Could you help me ? Thanks.
I checked the database, there is a table called ClientSecrets
there is a clientId and a value column with hashed value (maybe base64), and other columns also, but I didn't understand what's going on.
Related
I'm fairly confident I set everything up right for a multi-tenant app.
In Tenant A:
I created an Azure function and enabled a system managed identity. I granted the Managed Identity permissions to the Graph API. I confirmed my API can obtain an access token for the Managed Identity and access the Graph API with it. I added Azure AD authentication to the app and created an App Registration for the app. I configured the necessary Graph API permissions in the API Permissions settings of the app registration. I also enabled the various options to enable multi-tenant access.
In Tenant B:
I accessed the special URL to start the admin consent process for the app in Tenant A. I confirmed that I was prompted to consent to the permissions that I specified in the app registration in Tenant A. I can see that a couple new enterprise application entries were created in Tenant B for the app in Tenant A.
So far so good. However, no matter what I do, I cannot obtain a token to access the graph API in the context of Tenant B. It ALWAYS gives me a token for the managed identity in Tenant A, and accesses the info in Tenant A.
The code the retrieves an access token using the managed identity is here:
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions { AdditionallyAllowedTenants = { "*" }, TenantId = "<Tenant B ID>" });
var token = credential.GetToken(
new Azure.Core.TokenRequestContext(
new[] { "https://graph.microsoft.com/.default" }, null, null, "<Tenant B ID"));
var accessToken = token.Token;
Now, I have tried all sorts of different combinations here. But, from everything I read, I am supposed to be able to specify the Tenant ID of another tenant in these methods and obtain an access token for that specific tenant. However, even when I specify Tenant B's ID, I always get a token back for the managed identity in Tenant A. If I use the token, of course, I end up accessing all of Tenant A's information.
Ironically, if I remove the DefaultAzureCredentialOptions and include the tenantID in the GetToken request I get an error telling me that I'm trying to obtain a token for a different tenant (than my own) and that I need to add the AdditionallyAllowedTenants option. It clears the error up when I add that, but then it doesn't obtain a token for the other tenant.
Perhaps I am still approaching this wrong. I want to host a multi-tenant Azure Function that runs in the context of the Tenant where the request originated from to access information within that tenant using a managed identity that exists within that tenant. I can obtain the Tenant ID context from the claims sent to the Azure function during authentication, but no matter how I try to specify that Tenant ID in my code, it will not get a token for that Tenant.
UPDATE and SOLUTION:
Thanks to Philippe's great write up, I did finally get this. I had some confusion around the mechanisms at play here and want to add this note for clarity on how I solved it.
If you've followed most of the documents you have an Azure Function app in your "host" tenant A, with an associated app registration in tenant A. The app registration has certain API permissions configured, and you have consented to this app and permissions in Tenant B.
To access the resources in Tenant B, you need to create a secret key or certificate on the app registration in Tenant A. You then need to authenticate using the Client ID, and secret key/certificate of the app registration in Tenant A, but you will request a token for Tenant B. Here it is in code using a certificate for authentication:
var appToken = new ClientCertificateCredential(tenantID, appID, appCert, new ClientCertificateCredentialOptions { AdditionallyAllowedTenants = { "*" } });
var graphServiceClient = new GraphServiceClient(appToken);
Here, we assign the following to the variables:
tenantID = Tenant B's ID
appID = Tenant A's App registration client ID
appCert = Tenant A's App registration cert
We have to include the AdditionallyAllowedTenants config parameter, and the * authorizes a token from any tenant. We then take that credential and use it to build a new GraphServiceClient that is connected to Tenant B.
You've created two separate and independent identities for your service:
The managed identity: You don't need to deal with credentials, but this identity can only be used within the same tenant. You can't use your managed identity to directly access another tenant, and you can't (currently) use your managed identity to authenticate as your app registration.
The app registration: This identity can be used to access data in another tenant, but (currently) you do need to deal with credentials.
The DefaultAzureCredential will attempt different ways of authenticating until it finds one that works:
First, it first tries to authenticate using an EnvironmentCredential. This will look for certain environment variables needed to authenticate. Depending on the variables it finds, it will end up creating a ClientSecretCredential (to authenticate using the app's client secret) or ClientCertificateCredential (using an app's client certificate).
Next, if no suitable environment variables were found, it tries to authenticate using ManagedIdentityCredential, which (as the name suggests) will attempt to use a managed identity. For you, this will always succeed (because there is a managed identity available to be used).
Next, if no managed identity was available, it will continue trying with various other options. This is all described in detail in the documentation.
For your scenario, you basically have two options today.
Option 1, using DefaultAzureCredential and environment variables
Place the credentials for your multi-tenant app registration in environment variables and your existing code will "just work".
For example:
AZURE_TENANT_ID - the target tenant where you want to obtain the token
AZURE_CLIENT_ID - your multi-tenant app registration's app ID
AZURE_CLIENT_SECRET - a client secret for your multi-tenant app registration
Note: In general, it is better to use a certificate than a secret.
Option 2, using ClientSecretCredential or ClientCertificateCredential
Instead of using DefaultAzureCredential, you can directly create a ClientSecretCredential or a ClientCertificateCredential. You'll need to store the credential somewhere safe.
For example, on approach you could follow which would avoid any credentials in environment variable or in code:
Store credentials for your multi-tenant app in a key vault.
Allow your function's managed identity to access the credential in the key vault.
In your function, use your managed identity to retrieve the credential from Key Vault (e.g. using ManagedIdentityCredential), in your tenant.
Use that credential to authenticate as your multi-tenant app registration, in the target tenant (i.e. ClientSecretCredential or ClientCertificateCredential)
https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme
Managed Identities can only get tokens within the tenant that they exist in.
You'd need a "traditional" multi-tenant app registration + client certificate/secret for this case.
I'm working on a C# desktop application (a plugin for a 3D modelling software) and from this I need to access info from a GraphQL endpoint. In order to access the endpoint I need to provide a secret key. So what I've done is to store that key on Azure Key Vault.
And this is the code I'm using to get the key from Azure Key Vault:
var clientProd = new SecretClient(new Uri("my-azure-keyvault-url"),
new ClientSecretCredential(
"tenantId",
"clientId",
"clientSecret"));
Response<KeyVaultSecret> secret = await clientProd.GetSecretAsync("cbe-graphQL-prod");
But now I'm finding myself in this sort of vicious loop, where I'm not sure where that clientSecret should come from so it's not pushed to version control.
I'm going to deploy this application to multiple users in my company, so I wouldn't want to create an environment variable on everyone's machine (not even with a group policy).
What would be the best way to do this?
Thanks!
If you are hosting your application in Azure, you can use System Assigned Managed Identity to access the key vault without any credentials (client id, client secret)
If your application is hosted on premises, you can limit the access of the app registration (service principal) to the key vault in which secret is stored.
Then you can either store the client secret as environment variable or hard code the client secret
I have a WinForm App and an API which I secure with IdentityServer4. The client setup in IS4 is as follows, since there are no individual credentials, and only this application itself should have general access to the API.
new Client
{
ClientId = "ClientApp",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "APIOne" }
}
This works well so far, but I have a question about multiple instances of the WinForm app. Do all clients receive the same token? Because there are no individual credentials, only a client secret.
If I take a look into the tokens with https://jwt.io/ I see that they are different, but only the "exp" field, rest seems to be the same.
This works well so far, but I have a question about multiple instances of the WinForm app. Do all clients receive the same token?
No, in general they have not. But do note: if they would receive the same token it shouldn't matter.
The exp field is most likely "always" different, because it depends on the time it has been created, and therefor your token will not likely to be the same.
But, again; the token is most likely signed (or even encrypted). This is basically your safe-guard that the token is valid. Even if the tokens are equal, it should not matter. This means, the server for example, shouldn't expect the token to be unique.
Having said that; there is a catch.
It seems, that you now have a couple application which log's in with the same api key and secret.
I would suggest to add something, like a client id just to be able to identify the different clients. It would also make your token unique.
I'm calling my connect/token endpoint that identity server provides. When I'm entering my credentials to retrieve said access token, I am only getting back a partial token this long - 123456689080192830918230912830918203810928, without any periods in between. Unsure why the identity server is spitting this back out at me. Thanks.
In your client configuration (in the IdentityServer) when setting up clients (like here - defining the client section) there is an option, in the Client object: AccessTokenType
To see the full JWT token, this option should NOT be set to AccessTokenType.Reference
You either set it to AccessTokenType.Jwt, or you dont set it at all, because Jwt is the default value.
I am using Identity Server 3 and following example from here. Client Credentials using OAuth 2.0.
I overwritten AuthorizeAttribute but when I look at the ClaimsIdentity Name and Actor are null. Is this by design? I am responsible to populate them? If so how? I see that Claims has client_id but why it's not getting reflected in Name or Actor?
Q: How can I get identity of who is calling?
We have two definition in OAuth, User and Client
User is a human participant which basically has username and password.
Client is an application which User use which has a client_Id and client_secret.
When you using Client Credentials there is no User involved, and only clientId and ClientSecret is sent, therefor both Actor and Name property is null, because these properties are bind to user. In this case you should use Password, Code or Implicit Grant Type which have user involved.