I am trying to create an Azure Database programmatically using C# via the HTTP API.
I found this resource (in which he uses App Secret, rather than certificate), but can't get it to work due to a 403: Forbidden error.
If I use the token created in the 'try it' section of the azure docs, the code works fine, so it must be that the token generated doesn't have the permissions I need.
The secret I am trying to use was generated in Portal > Active Directory > App Registrations > {myapp} > Certificates and Secrets.
In API permissions I've added just about everything - though please suggest any I might have missed!
I opened the firewall to my IP and have spent hours looking for the answer, but can't seem to figure it out.
What am I missing please?
Although I believe it's permissions somewhere in Azure, here's my code:
/// <summary>
/// Create a new database
/// </summary>
/// <param name="subscriptionId">See Portal > Subscriptions (pick the right subscription!)</param>
/// <param name="resourceGroupName">The resource group the server was created in</param>
/// <param name="locationName">e.g. UK (South)</param>
/// <param name="sqlServeName">The name of the SQL Server VM created when the first database was created</param>
/// <param name="tenantId">See GetAccessToken</param>
/// <param name="clientId">See GetAccessToken</param>
/// <param name="clientSecret">See GetAccessToken</param>
/// <param name="databaseName">The name of the database to be created</param>
/// <returns></returns>
private async Task CreateDatabaseAsync(string subscriptionId, string resourceGroupName, string locationName, string sqlServeName, string tenantId, string clientId, string clientSecret, string databaseName)
{
var token = await GetAccessTokenAsync(tenantId, clientId, clientSecret);
var url = "https://management.azure.com/subscriptions/" + subscriptionId + "/resourceGroups/" + resourceGroupName + "/providers/Microsoft.Sql/servers/" + sqlServeName + "/databases/" + databaseName + "?api-version=2017-10-01-preview";
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Remove("Authorization");
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
string requestBody = "{location: \"" + locationName + "\"}";
HttpResponseMessage response = await httpClient.PutAsync(url, new StringContent(requestBody, Encoding.UTF8, "application/json"));
var statusCode = response.StatusCode.ToString();
}
/// <summary>
/// Gets and access token with which to call the Azure API
/// </summary>
/// <param name="tenantId">See Portal > Active Directory > Properties > Overview for this value</param>
/// <param name="clientId">See Portal > Active Directory > App Registrations > This App > Overview for this value</param>
/// <param name="clientSecret">See Portal > Active Directory > App Registrations > This App > Certificates & Secrets > Generate Secret </param>
/// <returns></returns>
private async Task<string> GetAccessTokenAsync(string tenantId, string clientId, string clientSecret)
{
Console.WriteLine("Begin GetAccessToken");
string authContextURL = "https://login.windows.net/" + tenantId;
var authenticationContext = new AuthenticationContext(authContextURL);
var credential = new ClientCredential(clientId, clientSecret);
var result = await authenticationContext
.AcquireTokenAsync("https://management.azure.com/", credential);
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the JWT token");
}
string token = result.AccessToken;
return token;
}
Managed to find the answer in this post/ this Microsoft Article.
What I needed to do was:
Go to Portal > Subscriptions > {my subscription}
Click on Access control (IAM)
Add > Add Role Assignment as shown below (note: 'SqlTest' is the Application I created in Portal > Azure Active Directory > App Registrations)
Then the code works nicely.
Related
I dont have a great understanding of Key Vault & certificates and struggling with an issue. I am making use of a PFX file to generate a JWT token to call an external webservice. Its working all right. Now I need to store the PFX in Key Vault and do the same.
I am uploading the cert using Az DevOps & Az Cli command
az keyvault certificate import --file $(filename.secureFilePath) --name pfx-cert-name --vault-name "keyvault-name" --password "password"
Now when I try to use the PFX in my .net core. I am using CertificateClient class & GetCertificateAsync methods to fetch the byte array of a PFX file.
var client = new CertificateClient(new Uri(kvUri), new DefaultAzureCredential()); var cert = await client.GetCertificateAsync(certName);
certInBytes = cert.Value.Cer;
The code fails. After doing online reading, I understand its because Get Certificate fetches the public details of the PFX file. Hence I started doing some reading online and doing import and download using Az Cli command on powershell.
I tried another technique to download original form of PFX using the below command:
az keyvault secret download --file inputCert.pfx --vault-name keyvault-name --encoding base64 --name pfx-cert-name
The command gives me another pfx but its still not the original form of PFX. When I try to use this cert to get JWT token, I get an error for invalid password.
I have two alternates, but I don't want to use either as they are not clean solutions:
Either store a byte array of PFX as a secret in key vault
Store base 64 encoded version of byte array of pfx for extra security.
To get the certificate with its private key, then you need to download it as a secret, not as a certificate. Yes, it does sounds weird, by that is how you do it.
This is the code I use to download a certificate with private key from AKV:
/// <summary>
/// Load a certificate (with private key) from Azure Key Vault
///
/// Getting a certificate with private key is a bit of a pain, but the code below solves it.
///
/// Get the private key for Key Vault certificate
/// https://github.com/heaths/azsdk-sample-getcert
///
/// See also these GitHub issues:
/// https://github.com/Azure/azure-sdk-for-net/issues/12742
/// https://github.com/Azure/azure-sdk-for-net/issues/12083
/// </summary>
/// <param name="config"></param>
/// <param name="certificateName"></param>
/// <returns></returns>
public static X509Certificate2 LoadCertificate(IConfiguration config, string certificateName)
{
string vaultUrl = config["Vault:Url"] ?? "";
string clientId = config["Vault:ClientId"] ?? "";
string tenantId = config["Vault:TenantId"] ?? "";
string secret = config["Vault:ClientSecret"] ?? "";
Console.WriteLine($"Loading certificate '{certificateName}' from Azure Key Vault");
var credentials = new ClientSecretCredential(tenantId: tenantId, clientId: clientId, clientSecret: secret);
var certClient = new CertificateClient(new Uri(vaultUrl), credentials);
var secretClient = new SecretClient(new Uri(vaultUrl), credentials);
var cert = GetCertificateAsync(certClient, secretClient, certificateName);
Console.WriteLine("Certificate loaded");
return cert;
}
/// <summary>
/// Helper method to get a certificate
///
/// Source https://github.com/heaths/azsdk-sample-getcert/blob/master/Program.cs
/// </summary>
/// <param name="certificateClient"></param>
/// <param name="secretClient"></param>
/// <param name="certificateName"></param>
/// <returns></returns>
private static X509Certificate2 GetCertificateAsync(CertificateClient certificateClient,
SecretClient secretClient,
string certificateName)
{
KeyVaultCertificateWithPolicy certificate = certificateClient.GetCertificate(certificateName);
// Return a certificate with only the public key if the private key is not exportable.
if (certificate.Policy?.Exportable != true)
{
return new X509Certificate2(certificate.Cer);
}
// Parse the secret ID and version to retrieve the private key.
string[] segments = certificate.SecretId.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length != 3)
{
throw new InvalidOperationException($"Number of segments is incorrect: {segments.Length}, URI: {certificate.SecretId}");
}
string secretName = segments[1];
string secretVersion = segments[2];
KeyVaultSecret secret = secretClient.GetSecret(secretName, secretVersion);
// For PEM, you'll need to extract the base64-encoded message body.
// .NET 5.0 preview introduces the System.Security.Cryptography.PemEncoding class to make this easier.
if ("application/x-pkcs12".Equals(secret.Properties.ContentType, StringComparison.InvariantCultureIgnoreCase))
{
byte[] pfx = Convert.FromBase64String(secret.Value);
return new X509Certificate2(pfx);
}
throw new NotSupportedException($"Only PKCS#12 is supported. Found Content-Type: {secret.Properties.ContentType}");
}
}
The code above depends on these NuGet packages:
Azure.Security.KeyVault.Certificates
Azure.Security.KeyVault.Keys
Azure.Security.KeyVault.Secrets
There are 2 ways to solve this problem. One with the help of DefaultCredentials class while the other solution being with the help of a SPN using class ClientSecretCredentials.
I have written a detailed article on both the solution. Since the original problem was in regards to DefaultCredentials, I wrote about it first
https://blog.devgenius.io/fetch-pfx-cert-from-key-vault-using-defaultcredentials-3795bd23d294?sk=be8a6fea080ff19056a0b90fc9532cd7
https://blog.devgenius.io/fetch-pfx-cert-from-key-vault-using-clientsecretcredentials-c0e80b129b37?sk=63c93f776bde72f49ef12263838e8d82
Currently i am trying to implement the azure active directory authentication by passing user name and password. So for this i have trying to get the access toke but facing issue to get the same. If i use the client id and client secret then i am able to get the token but when i try to by passing username and password then its not giving the result and throwing the exception :
"error":"invalid_client","error_description":"AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion'
Below the code which i am using for this:
/// <summary>
/// Working with client id and client secret
/// </summary>
/// <returns></returns>
public async Task<string> GetTokenUsingClientSecret()
{
//authentication parameters
string clientID = "XXXXXXXXXXXXXXXXXXXXXXXXXX";
string clientSecret = "XXXXXXXXXXXXXXXXXXXXXXXXX";
string directoryName = "xxx.onmicrosoft.com";
var credential = new ClientCredential(clientID, clientSecret);
var authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/" + directoryName, false);
var result = await authenticationContext.AcquireTokenAsync("https://management.core.windows.net/", clientCredential: credential);
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the JWT token");
}
string token = result.AccessToken;
return token;
}
/// <summary>
/// Not Working with username and password.
/// </summary>
public async Task<string> GetTokenUsingUserNamePassword()
{
try
{
string user = "username.onmicrosoft.com";
string pass = "yourpassword";
string directoryName = "XXXX.onmicrosoft.com";
string authority = "https://login.microsoftonline.com";
string resource = "https://management.core.windows.net/";
string clientId = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
var credentials = new UserPasswordCredential(user, pass);
var authenticationContext = new AuthenticationContext($"{authority}/{directoryName}");
var result = authenticationContext.AcquireTokenAsync(resource: resource, clientId: clientId, userCredential: credentials);
return result.Result.AccessToken;
}
catch (Exception ex)
{
throw ex;
}
}
AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion'.
According to your mentioned exception, I assume that you registry an Azure AD Web app/API application. Please have a try to use an Azure AD native appilcation then it should work. More details you could refer to this document -Constraints & Limitations section.
No web sites/confidential clients
This is not an ADAL limitation, but an AAD setting. You can only use those flows from a native client. A confidential client, such as a web site, cannot use direct user credentials.
How to reigstry Azure AD native Application.
I'm using Windows Azure Active Directory Authentication. This is used to secure a c# windows service that calls a c# Web API service in Azure.It has worked for quite some time but now I've started getting the following exception:
Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware Error: 0 : Authentication failed
System.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
IsReadOnly = False,
Count = 2,
Clause[0] = X509ThumbprintKeyIdentifierClause(Hash = 0x61B44041161C13F9A8B56549287AF02C16DDFFDB),
Clause[1] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
)
I have no idea what this means or how to fix it :(
Update
In answer to the comment about key rollover my web service is using the following code:
private void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
Tenant = ConfigurationManager.AppSettings["ida:Tenant"]
});
}
which according to that link means it should be secured against this type of issue.
The start of the token including the kid looks like this:
token: '{"typ":"JWT","alg":"RS256","x5t":"YbRAQRYcE_motWVJKHrwLBbd_9s","kid":"YbRAQRYcE_motWVJKHrwLBbd_9s"}
Update 2
My code to acquire token in the windows service:
internal string GetAuthorizationToken()
{
string authority = String.Format(aadInstance, tenant);
var authContext = new AuthenticationContext(authority);
var authResult = AcquireToken(authContext);
return authResult == null ? null : authResult.CreateAuthorizationHeader();
}
/// <summary>
/// Acquires the token.
/// </summary>
/// <param name="authContext">The authentication context.</param>
/// <returns>Authentication Result</returns>
private AuthenticationResult AcquireToken(AuthenticationContext authContext)
{
try
{
return authContext.AcquireTokenAsync(apiResourceId, clientId, new UserPasswordCredential(user, pass)).Result;
}
catch (Exception)
{
return null;
}
}
This exception would occur when the application trying to find the signing key via the key identity in the token from Azure, however the key on Azure already rollover.
Please get a new access token in the windows service to fix this issue.
I am working on a requirement where I need to create a site collection in SharePoint using client side api. I know server side we can do it using self service site creation api. Also I know in case of SharePoint Online , we have Microsoft.Online.SharePoint.Client.Tenant.dll that we can use to create site collection However in my case I have a On premise environment (SharePoint 2013) where I need to create a site collection thru client side api. Can you please let me know if there is any API that I can use for this requirement.
Thanks for any Help you can provide on this.
This is not possible to do by using the CSOM, on an on-premise environment.
As you mentioned, it is possible on the SPO environment using the library that you listed (Microsoft.Online.SharePoint.Client.Tenant.dll).
I'm not sure if this will help, but here is code that could create a site inside of the current site collection:
You will also need to add using statements for System.Collections.Generic and System.Text.
// Starting with ClientContext, the constructor requires a URL to the
// server running SharePoint.
ClientContext context = new ClientContext("http://SiteUrl");
WebCreationInformation creation = new WebCreationInformation();
creation.Url = "web1";
creation.Title = "Hello web1";
Web newWeb = context.Web.Webs.Add(creation);
// Retrieve the new web information.
context.Load(newWeb, w => w.Title);
context.ExecuteQuery();
label1.Text = newWeb.Title;
This code was taken directly from here: http://msdn.microsoft.com/en-us/library/fp179912.aspx
How to create site collection via SharePoint 2013 Managed CSOM
Tenant.CreateSite method from Microsoft.Online.SharePoint.Client.Tenant.dll assembly is intended for site collection creation:
/// <summary>
/// Create a new site.
/// </summary>
/// <param name="context"></param>
/// <param name="url">rootsite + "/" + managedPath + "/" + sitename: e.g. "https://auto.contoso.com/sites/site1"</param>
/// <param name="title">site title: e.g. "Test Site"</param>
/// <param name="owner">site owner: e.g. admin#contoso.com</param>
/// <param name="template">The site template used to create this new site</param>
/// <param name="localeId"></param>
/// <param name="compatibilityLevel"></param>
/// <param name="storageQuota"></param>
/// <param name="resourceQuota"></param>
/// <param name="timeZoneId"></param>
internal static void CreateSite(ClientContext context, String url, String owner, String title =null, String template = null, uint? localeId = null, int? compatibilityLevel = null, long? storageQuota = null, double? resourceQuota = null, int? timeZoneId = null)
{
var tenant = new Tenant(context);
if (url == null)
throw new ArgumentException("Site Url must be specified");
if (string.IsNullOrEmpty(owner))
throw new ArgumentException("Site Owner must be specified");
var siteCreationProperties = new SiteCreationProperties {Url = url, Owner = owner};
if (!string.IsNullOrEmpty(template))
siteCreationProperties.Template = template;
if (!string.IsNullOrEmpty(title))
siteCreationProperties.Title = title;
if (localeId.HasValue)
siteCreationProperties.Lcid = localeId.Value;
if (compatibilityLevel.HasValue)
siteCreationProperties.CompatibilityLevel = compatibilityLevel.Value;
if (storageQuota.HasValue)
siteCreationProperties.StorageMaximumLevel = storageQuota.Value;
if (resourceQuota.HasValue)
siteCreationProperties.UserCodeMaximumLevel = resourceQuota.Value;
if (timeZoneId.HasValue)
siteCreationProperties.TimeZoneId = timeZoneId.Value;
var siteOp = tenant.CreateSite(siteCreationProperties);
context.Load(siteOp);
context.ExecuteQuery();
}
//Usage
const string username = "***#***.onmicrosoft.com";
const string password = "***";
const string tenantAdminUrl = "https://***-admin.sharepoint.com/";
const string newSiteCollUrl = "https://contoso.sharepoint.com/sites/finance"
var securedPassword = new SecureString();
foreach (var c in password.ToCharArray()) securedPassword.AppendChar(c);
var credentials = new SharePointOnlineCredentials(username, securedPassword);
using (var context = new ClientContext(tenantAdminUrl))
{
context.Credentials = credentials;
CreateSite(context, newSiteCollUrl,username);
}
It is available in the April 2014 CU
http://blogs.msdn.com/b/vesku/archive/2014/06/09/provisioning-site-collections-using-sp-app-model-in-on-premises-with-just-csom.aspx
I am trying to programmatically add a user like this below but get an access denied message on the Save. I'm running locally on Windows 7 and the code resides in a console app.
/// <summary>
///
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="description"></param>
public static void CreateUser(string userName, string password, string description)
{
PrincipalContext pc = new PrincipalContext(ContextType.Machine, null);
System.DirectoryServices.AccountManagement.UserPrincipal u = new UserPrincipal(pc);
u.SetPassword(password);
u.Name = userName;
u.Description = description;
u.UserCannotChangePassword = true;
u.PasswordNeverExpires = true;
u.Save();
GroupPrincipal gp = GroupPrincipal.FindByIdentity(pc, "Users");
gp.Members.Add(u);
gp.Save();
}
Any ideas? I tried supplying an administrators username and password and still get the same error.
The console app gets executed like this:
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.UserName = userName;
startInfo.Password = securePassword;
startInfo.LoadUserProfile = true;
startInfo.UseShellExecute = false;
startInfo.FileName = batchPath;
startInfo.Arguments = operationLogID.ToString();
Process.Start(startInfo);
Here is a rough view of how the code is set up:
Console App test harness gets executed in debug mode.
I check for a user and if they don't exist..then I try and create it shown above. This is where the error occurs.
Even if you're logged in as admin, you need to run your console as admin. Here's how to launch a console as admin: http://www.howtogeek.com/howto/windows-vista/run-a-command-as-administrator-from-the-windows-vista-run-box/.
Then find your console app and run it.
Good luck!
-Michael