Azure DevOps API for create new projects and pipeline - c#

In my organization there are a lot of researchers and each of them has few projects. For each project, I have to create a repository in Azure DevOps and a pipeline. I'm using .NET Core 3.1 but I can update to .NET5.
Apart from the name, the pipeline is the same for each project. What I want to create is a simple internal website where each researcher can add its project number and automatically the website call Azure DevOps via API to create a new repository and a new pipeline.
I tried to run the project from the Microsoft repository on GitHub but the examples don't work. What I want to achieve is a very simple piece of code that can create a new repository with an associate pipeline.
I saw the branch OAuthWebSampleAspNetCore.csproj in the same Microsoft repository. There is a OAuthController that seems correct based on the Microsoft documentation but it doesn't work. The following code creates the request to Azure DevOps.
private String BuildAuthorizationUrl(String state)
{
UriBuilder uriBuilder = new UriBuilder(this.Settings.AuthorizationUrl);
var queryParams = HttpUtility.ParseQueryString(uriBuilder.Query ?? String.Empty);
queryParams["client_id"] = this.Settings.ClientApp.Id.ToString();
queryParams["response_type"] = "Assertion";
queryParams["state"] = state;
queryParams["scope"] = this.Settings.ClientApp.Scope;
queryParams["redirect_uri"] = this.Settings.ClientApp.CallbackUrl;
uriBuilder.Query = queryParams.ToString();
return uriBuilder.ToString();
}
This is the URL the application calls
https://app.vssps.visualstudio.com:443/oauth2/authorize?client_id=myClientId&response_type=Assertion&state=6ca228c6-f73f-48be-9a0a-38c8f2483837&scope=myListOfScopes&redirect_uri=https%3a%2f%2flocalhost%3a43742%2foauth%2fcallback
and this is the result from Azure DevOps.
The next part is the callback. Based on the Microsoft documentation, I have to call the token URL https://app.vssps.visualstudio.com/oauth2/token as POST. The following code is how I do the request.
public async Task<ActionResult> Callback(String code, Guid state)
{
TokenViewModel tokenViewModel = new TokenViewModel() { OAuthSettings = this.Settings };
string error;
if (ValidateCallbackValues(code, state.ToString(), out error))
{
// Exchange the auth code for an access token and refresh token
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, this.Settings.TokenUrl);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Dictionary<String, String> form = new Dictionary<String, String>()
{
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
{ "client_assertion", GetClientAppSecret() },
{ "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" },
{ "assertion", code },
{ "redirect_uri", this.Settings.ClientApp.CallbackUrl }
};
requestMessage.Content = new FormUrlEncodedContent(form);
HttpResponseMessage responseMessage = await s_httpClient.SendAsync(requestMessage);
if (responseMessage.IsSuccessStatusCode)
{
String body = await responseMessage.Content.ReadAsStringAsync();
Token token = s_authorizationRequests[state];
JsonConvert.PopulateObject(body, token);
tokenViewModel.Token = token;
}
else
{
error = responseMessage.ReasonPhrase;
}
}
else
{
tokenViewModel.Error = error;
}
return View("TokenView", tokenViewModel);
}
Although, I think, I have a valid token and IsSuccessStatusCode is always false because a BadRequest.
Are there any updated samples I can use? Did someone face the same issues?

https://app.vssps.visualstudio.com:443/oauth2/authorize?client_id=myClientId&response_type=Assertion&state=6ca228c6-f73f-48be-9a0a-38c8f2483837&scope=myListOfScopes&redirect_uri=https%3a%2f%2flocalhost%3a43742%2foauth%2fcallback
I found you used your localhost as your Callback URL.
You cannot use your localhost as the callback url, for your localhost is unavailable to azure devops services.
You have to use a callback url that can be accessed from the public network. Or you will get above 400-bad request error.

Related

how to create a web service for microsoft graph (C#)

I've recently started working as a junior c# developer. My boss asked me to build methods to CRUD teams in our AAD using the microsoft graph API. I've achieved this with a test application like that:
public async Task<string> createTeam()
{
// readying data from registry
var clientId = "********************"; //application (client) ID
var clientSecret = "********************";
var redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient";
var authority = "https://login.microsoftonline.com/********************/v2.0";
var cca = ConfidentialClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.Build();
List<string> scopes = new List<string>
{
"https://graph.microsoft.com/.default"
};
//
var authenticationProvider = new MsalAuthenticationProvider(cca, scopes.ToArray());
//
GraphServiceClient graphClient = new GraphServiceClient(authenticationProvider);
// Code to create a Team
var team = new Team
{
DisplayName = "0000My Sample Team",
Description = "My Sample Team’s Description",
AdditionalData = new Dictionary<string, object>()
{
{"template#odata.bind", "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"}
}
};
var result = await graphClient.Teams.Request().AddAsync(team);
return result.DisplayName;
}
With that piece of code working, I've created an asp.net web Application (.net framework) and added the class to it. The plan was to deploy it to an IIS server and and publish the methods as web services.
[WebMethod]
public async Task<string> createTeamAsync()
{
//The class where my methods reside
TeamServices ts = new TeamServices();
var result = await ts.createTeam();
return "OK";
}
I registered the app and deployed but when I try to use it, it does not create any Team.
Do you know what I'm doing wrong of what I should learn next get the app working? Im just a few weeks in c# and I'm completely lost in all that .net echosystem
Thanks in advance

Which API security to use for an external API consumed by Blazor WASM

Context: Got an API running with a simple /auth call that expects email, password and some sort of db identifier. Which then returns a JWT token. This token can be used to request the other calls and know which database to access. The client is now in UWP which handles the UI and does the calls to the API. Not using Azure Api Management for now and not using the Microsoft Identity platform. Just a regular password hash check.
Recently, we wanted to switch from UWP to a Blazor WASM (client only) but haven't really found any suitable support to work with Bearer tokens and the documentation steers us towards four options.
AAD
AAD B2C
Microsoft Accounts
Authentication library (?)
Not all our users have Office 365 accounts.
Kind of lost in this new "Blazor space" since it's very different from our WPF & UWP projects and it doesn't seem to be fully documented yet.
Thanks.
Update code on request
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// Local storage access
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddTransient<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddTransient<IAccessTokenProvider, ApiTokenProvider>();
builder.Services
.AddHttpClient<IMambaClient, MambaClient>(client => client.BaseAddress = _baseUri)
.AddHttpMessageHandler(sp => sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>()
.ConfigureHandler(new[] { _apiEndpointUrl }));
await builder.Build().RunAsync();
}
ApiTokenProvider.cs
public class ApiTokenProvider : IAccessTokenProvider
{
private readonly ILocalStorageService _localStorageService;
public ApiTokenProvider(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
public async ValueTask<AccessTokenResult> RequestAccessToken()
{
var token = await _localStorageService.GetItemAsync<string>("Token");
AccessTokenResult accessTokenResult;
if (!string.IsNullOrEmpty(token))
{
accessTokenResult = new AccessTokenResult(AccessTokenResultStatus.Success, new AccessToken() { Value = token, Expires = new DateTimeOffset(DateTime.Now.AddDays(1)) }, "/");
}
else
{
accessTokenResult = new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect, new AccessToken() { Value = token, Expires = new DateTimeOffset(DateTime.Now.AddDays(1)) }, "/login");
}
return await new ValueTask<AccessTokenResult>(accessTokenResult);
}
public ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
{
throw new NotImplementedException();
}
}
New question: How will I be able to call POST /auth now if this would work? I would get an error since I don't have a token yet for this TypedClient and adding another typed client isn't possible since I cannot give it a different name?

How and where do I add a new Schema Extension?

I have been trying for weeks to add a new Schema Extension for my Microsoft Graph based MVC application, essentially to store some basic variables along with a mail Message.
I've followed this example from GitHub and after some very frustrating days of working out that "Boolean" & "Integer" weren't supported property types, I then ran into the fabled "Insufficient privileges to complete the operation"...
I have been pulling my hair out trying to work out how and where I'm supposed to add my new extension, as it stands I'm trying to add it with the below code as an authenticated user (who is an admin):
SchemaExtension extensionPayload = new SchemaExtension()
{
Description = "my extension example",
Id = $"myExtensionExample",
Properties = new List<ExtensionSchemaProperty>()
{
new ExtensionSchemaProperty() { Name = "prop1", Type = "String" },
new ExtensionSchemaProperty() { Name = "prop2", Type = "String" }
},
TargetTypes = new List<string>()
{
"Message"
}
};
SchemaExtension test = await client
.SchemaExtensions
.Request()
.AddAsync(extensionPayload);
My Graph Client is generated with the below code:
public static async Task<GraphServiceClient> GetClient(HttpContextBase context)
{
string token = await GetAccessToken(context);
GraphServiceClient client = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return Task.FromResult(0);
}
)
);
return client;
}
And my Oauth config requests the following permissions:
<add key="ida:AppScopes" value="User.Read Mail.ReadWrite Mail.Send Contacts.ReadWrite Directory.AccessAsUser.All" />
I've checked the Azure App Permissions of the account I'm testing with and they all appear to be correct? Is that where they're supposed to be??
ANY pointers would be greatly appreciated, as I've lost so much time trying to get what I thought was a very straight forward test app up and running.
According to the docs, using this call with Application permissions isn't supported.

Programmatically register app in azure active directory using graph api

I am trying to register an application in Azure AD using graph API, I have a method CallRestAPI which will make the request.
Below is the code
public async Task<Response> AzureADApp()
{
Response responseMessage = new Response();
try
{
var token = GenerateToken();
List<(string, string)> listHeaders = new List<(string, string)>();
listHeaders.Add(("Authorization", string.Concat("Bearer" + " " + token)));
listHeaders.Add(("Content-Type", "application/json"));
List<(string, string)> param = new List<(string, string)>();
param.Add(("displayName", "VS1Application123"));
param.Add(("homepage", "https://localhost:44358/"));
param.Add(("identifierUris", "https://G7CRM4L/6958490c-21ae-4885-804c-f03b3add87ad"));
string callUrl = "https://graph.windows.net/G7CRM4L/applications/?api-version=1.6";
var result = CallRestAPI(callUrl, "", Method.POST, listHeaders, param);
}
catch (Exception ex)
{
responseMessage.StatusCode = Convert.ToInt16(HttpStatusCode.InternalServerError);
}
return responseMessage;
}
public async Task<IRestResponse> CallRestAPI(string BaseAddress, string SubAddress, Method method, List<(string, string)> headersList = null, List<(string, string)> paramsList = null)
{
var call = new RestClient(BaseAddress + SubAddress);
var request = new RestRequest(method);
if (headersList != null)
{
foreach (var header in headersList)
{
request.AddHeader(header.Item1, header.Item2);
}
}
if (paramsList != null)
{
foreach (var param in paramsList)
{
request.AddParameter(param.Item1, param.Item2);
}
}
var response = call.ExecuteTaskAsync(request);
return response.Result;
}
I think the way I am sending parameters in the body is not correct can anyone guide me how to make this code work or is there a better way to achieve the same?
Thank you.
A better way to achieve the same i.e. register an app with Azure AD will be to make use of Azure AD Graph Client Library
I say it's a better approach because when you use the client library you reap multiple benefits like no raw HTTP request handling, writing more convenient and declarative C# code, depending on a well tested library, async support etc.
Underlying Graph API used will still be the same I suppose
POST https://graph.windows.net/{tenant-id}/applications?api-version=1.6
Here is sample code (C#) to create an Azure AD application
Notice that I've kept app.PublicClient flag as true to register as a native application. You can set it to false if you want to register it as a web application.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.ActiveDirectory.GraphClient;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace CreateAzureADApplication
{
class Program
{
static void Main(string[] args)
{
ActiveDirectoryClient directoryClient;
ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(new Uri("https://graph.windows.net/{yourAADGUID}"),
async () => await GetTokenForApplication());
Application app = new Application();
app.DisplayName = "My Azure AD Native App";
app.PublicClient = true;
app.Homepage = "https://myazureadnativeapp";
activeDirectoryClient.Applications.AddApplicationAsync(app).GetAwaiter().GetResult();
}
public static async Task<string> GetTokenForApplication()
{
AuthenticationContext authenticationContext = new AuthenticationContext(
"https://login.microsoftonline.com/{yourAADGUID}",
false);
// Configuration for OAuth client credentials
ClientCredential clientCred = new ClientCredential("yourappclientId",
"yourappclientsecret"
);
AuthenticationResult authenticationResult =
await authenticationContext.AcquireTokenAsync("https://graph.windows.net", clientCred);
return authenticationResult.AccessToken;
}
}
}
Setup: I have an application registered in Azure AD, which has required permissions as application permission - Read and Write all applications and grant permissions is done for this app. Now using this application's client id and client secret, a token is acquired and Azure AD Graph API is called to create an application. It is not mandatory to use application permissions, you can also use delegated permissions by prompting user for credentials. See links to more detailed examples (old ones but still useful).
Console Application using Graph client library
Web app calls Graph using Graph client library
Azure AD Graph Client Library 2.0 Announcement page
On a side note, you could do this using the newer Microsoft Graph API as well,
POST https://graph.microsoft.com/beta/applications
but the ability to create applications is still in beta and hence not recommeded for production workloads. So even though Microsoft Graph API would be recommende for most scenarios, at least for this one, using Azure AD Graph API is the way to go currently.
I have covered this in a little more detail in a similar SO Post here.

Swagger UI will return API data, but my Authorized calls return "permission denied"

So I believe my APIservice should be fine since I can return results through Swagger? I am calling from a WPF project. I launch the program and it asks me to login, then it continues and will tell me I don't have permission.
I'm super green to WebAPI2 and think I may just be constructing my call incorrectly. It does seem that I get a token back correctly from my site, the only issue is when I try to actually call on the API for data.
Here is my code:
public static string clientId = "{#Calling App Id}";
public static string commonAuthority = "https://login.windows.net/{#my Azure AD tenant}";
public static Uri returnUri = new Uri("http://MyDirectorySearcherApp");
const string ResourceUri = "https://{#Api App Service}.azurewebsites.net";
public static async Task<List<User>> LoadBands(IPlatformParameters parent)
{
AuthenticationResult authResult = null;
List<User> results = new List<User>();
try {
//get token or use refresh
AuthenticationContext authContext = new AuthenticationContext(commonAuthority);
if (authContext.TokenCache.ReadItems().Count() > 0)
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
authResult = await authContext.AcquireTokenAsync(ResourceUri, clientId, returnUri, parent);
} catch (Exception ee) {
throw ex;
}
using (var httpClient = new HttpClient()) {
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{ResourceUri}/api/Band/")) {
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
using (var response = await httpClient.SendAsync(request)) {
string responseData = await response.Content.ReadAsStringAsync();
//responseData always equals "You do not have permission to view this directory or page"
return results;
}
}
}
Edit: Maybe helpful to note I'm using a DataAPI that is called by a Rest API, the rest API is secured by Azure AD.
Edit: I'm calling from a Portable Class Library.
Edit: Well, I'm getting authenticated but it does not appear to make any difference. If I completely remove the Auth header I get the same result
It seems that the token is incorrect for the web API which protected by Azure AD. Please check the aud claim in the token which should match the Audience you config in the web API project. You can check the aud claim by parse the token from this site.
And if you still have the problem please share the code how you protect the web API.
Update
If you were using the Express mode like below, you need to acquire the access_token using the app which you associate with the web API.
If you were using the Advanced mode, we should also use the that app to acquire the token and the ResourceUri should matched the value you config in ALLOWED TOKEN AUDIENCES like below:

Categories