How and where do I add a new Schema Extension? - c#

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.

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

How to use a custom SSO in my other apps?

I hit a wall trying to use a custom SSO in my apps so I figured I'd take a step back and ask questions here.
I implemented a RESTful Authentication API in .Net 5.0 (without Identity) by following this article.
I can call http://localhost:4001/api/auth/login with an email and a password to get a JWT in response
{
"token": "eyJhb<--removed jwt-->",
"errors": null
}
Is this a good approach? I didn't want to use something as bulky as Identity or as complicated as or OAuth.
How do I tie this up with a normal authentication "flow"? I know that you have to Challenge the identity provider while logging in, but I couldn't find any exemples on how to do it that would fit my situation. (Itried to implement a custom scheme with builder.AddJwtBearer("my_scheme", ... but this is where I hit a wall)
I managed to do what I wanted, so if anyone stumbles upon this:
Turns out that I was making this out to be super complicated when it's actually quite simple...
The point was to make things easy, so trying to create en authentication scheme was off topic.
Here is the final code:
using var client = new HttpClient() { BaseAddress = new Uri("http://localhost:4001/api/") };
try
{
var stringContent = new StringContent(JsonSerializer.Serialize(new { email = model.Email, password = model.Password }), Encoding.UTF8, "application/json");
var result = await client.PostAsync("auth/login", stringContent);
var jsonResponse = await result.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var authResult = JsonSerializer.Deserialize<AuthResult>(jsonResponse, options);
var jwt = authResult.Token.Jwt;
var isValidJwt = JwtHelper.ValidateCurrentToken(jwt, _myExternalAuthSettings.RsaPub, _myExternalAuthSettings.Issuer);
if (isValidJwt)
{
HttpContext.Response.Cookies.Append("my_auth", jwt, new CookieOptions()
{
Domain = "localhost",
Expires = DateTimeOffset.FromUnixTimeSeconds(authResult.Token.UnixTimeExpiresAt)
});
// finish authenticating the user
}
// something failed
}
Note: I followed this article to be able to locally validate the token with the public key (the private key is used to create it).
Cheers

Azure DevOps API for create new projects and pipeline

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.

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?

Get user Id from reference token in API

My setup,
An IdentityServer using MVC Identity to store the Users, created with dotnet new mvc -au Individual and applying the http://docs.identityserver.io/en/release/quickstarts/0_overview.html tutorial, running in localhost 5000.
A client App, but now I'm using postman to do tests.
A WEB API, created with dotnet new webapi, running in localhost 5001.
The IdentityServer resources and clients configuration is the following, notice that I'm using reference tokens:
public static IEnumerable<IdentityResource> GetIdentityResources() {
return new List<IdentityResource>{ new IdentityResources.OpenId() };
}
public static IEnumerable<ApiResource> GetApiResources() {
return new List<ApiResource>{
new ApiResource("api_resource", "API Resource") {
Description= "API Resource Access",
ApiSecrets= new List<Secret> { new Secret("apiSecret".Sha256()) },
}
};
}
public static IEnumerable<Client> GetClients() {
return new List<Client>{
new Client {
ClientId= "angular-client",
ClientSecrets= { new Secret("secret".Sha256()) },
AllowedGrantTypes= GrantTypes.ResourceOwnerPassword,
AllowOfflineAccess= true,
AccessTokenType = AccessTokenType.Reference,
AlwaysIncludeUserClaimsInIdToken= true,
AllowedScopes= { "api_resource" }
}
}
The password and user is send with postman and the token received is send to the WEB API also with postman, something like call localhost:5001/v1/test with the token pasted in option bearer token.
In the API Startup, in ConfigureServices I'm adding the lines below
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority= "http://localhost:5000";
options.ApiName= "api_resource";
options.ApiSecret = "apiSecret";
});
And I'm getting the Id of the user inside the controller as follows:
public async Task<IActionResult> Get(int id) {
var discoveryClient = new DiscoveryClient("http://localhost:5000");
var doc = await discoveryClient.GetAsync();
var introspectionClient = new IntrospectionClient(
doc.IntrospectionEndpoint,
"api_resource",
"apiSecret");
var token= await HttpContext.GetTokenAsync("access_token");
var response = await introspectionClient.SendAsync(
new IntrospectionRequest { Token = token });
var userId = response.Claims.Single(c => c.Type == "sub").Value;
}
The question itself is, am I using the right path to get the Id from the reference token?, because now It works but I don't want to miss anything, specially thinking that is a security concern.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference tokens.
Thanks in advance.
Inside a controller action that is protected with an [Authorize] attribute you can simply get claims directly from the ClaimsPrinciple, without having to go through a manual discovery client. The claims principle is handily aliased simply with User inside your controllers.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type ==
ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference
tokens.
It works just fine with reference tokens. You should have no problems accessing the sub claim.
EDIT:
As I mentioned in a comment below, I tend to use the standard JwtClaimTypes and create some extension methods on the ClaimsPrinciple, such as:
public static string GetSub(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Subject))?.Value;
}
or
public static string GetEmail(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Email))?.Value;
}
... so that within my protected actions I can simply use User.GetEmail() to get hold of claim values.
It's worth stating the obvious, that any method for retrieving claim values will only work if the claims actually exist. i.e. asking for the ZoneInfo claim will not work unless that claim was requested as part of the token request in the first place.

Categories