How to use OAuth with Swagger and NSwagStudio - swagger

I'm trying to generate a C# client for an API that has provided a swagger.json file to me, located at this link;
https://api.ekm.net/swagger/v1/swagger.json
Using the NSwagStudo application I am able to import the configuration file and generate a file called Client.cs which implements a class called Client and it has methods on it that match the API.
However when I call any of the methods I get an "Unauthorized" exception and I can not find any way to provide the OAuth key and secret to the client or anyone doing similar with other authentication methods.
Inspecting the swagger configuration files does show that OAuth is indicated as the authentication method as follows;
"securityDefinitions": {
"OAuth": {
"flow": "accessCode",
"authorizationUrl": "https://api.ekm.net/connect/authorize",
"tokenUrl": "https://api.ekm.net/connect/token",
"scopes": {
"tempest.customers.read": "Read a shop's customers.",
"tempest.customers.write": "Modify a shop's customers.",
"tempest.orders.read": "Read a shops orders.",
"tempest.orders.write": "Modify a shop's orders.",
"tempest.products.read": "Read a shop's products.",
"tempest.products.write": "Modify a shop's products.",
"tempest.categories.read": "Read a shop's categories.",
"tempest.categories.write": "Modify a shop's categories.",
"tempest.settings.orderstatuses.read": "Read a shop's order statuses.",
"tempest.settings.domains.read": "Read a shop's domains."
},
"type": "oauth2",
"description": "In order to ensure the safety of our users data, we require all partner applications to register via the [Partner Dashboard](https://partners.ekm.net/). Once registered, partners are provided with an application key, which can be used during an OAuth2 handshake to create a token. This token can then used to make requests on behalf of a merchant."
}
},
My test code is as follows;
static void Main(string[] args)
{
var swagClient = new Client();
var ords = swagClient.ApiV1OrdersGetAsync(1, 100).Result; // This call throws SwaggerException: Unauthorized
}
The Client class does not have any obvious methods or properties to set the security values or any constructor parameters.
Does anyone have an example of how to achieve this?

I agree. It's kind of strange that it doesn't just accept some kind of "insert JWT here".
Anyway, this is how I've fixed it:
Inject HttpClient
Tick on the box named "Inject HttpClient via constructor" in NSwagStudio
CustomMessageHandler
Introduce a custom HttpMessageHandler:
internal class AuthTokenHttpMessageHandler: HttpClientHandler
{
private readonly Action<HttpRequestMessage, CancellationToken> _processRequest;
public AuthTokenHttpMessageHandler(Action<HttpRequestMessage, CancellationToken> processRequest)
{
_processRequest = processRequest;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_processRequest(request, cancellationToken);
return base.SendAsync(request, cancellationToken);
}
}
This handler accepts a delegate in which you can provide your JWT.
Integrating with your client
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
public class MyService : IDisposable
{
private readonly AuthTokenHttpMessageHandler _messageHandler;
private readonly HttpClient _httpClient;
private readonly MyNSwagClient _client;
public MyService()
{
_messageHandler = new AuthTokenHttpMessageHandler((req, _) =>
{
req.Headers.Authorization = new AuthenticationHeaderValue("bearer", "your token goes here");
});
_httpClient = new HttpClient(_messageHandler);
_client = new MyNSwagClient(_httpClient);
}
public async Task<SomeModel> GetStuffAsync(string paramenter1)
{
return await _client.StuffGetAsync(parameter1);
}
public void Dispose()
{
_httpClient?.Dispose();
_messageHandler?.Dispose();
}
}
I hope this helps you

Related

Blazor Server GraphServiceClient not authenticated in wrapper library

I have a Blazor Server application that authenticates users through AAD. I'm using the Microsoft Graph SDK to query user info such as photo, etc., which I can do using DI to get the GraphServiceClient directly within a component in the Blazor Server app.
[Inject] private GraphServiceClient GraphServiceClient { get; set; }
However, for testing purposes, I've created a wrapper around the GraphServiceClient called IGraphService in another library within the same solution. This is where the problem occurs and the GraphServiceClient fails authentication with:
No account or login hint was passed to the AcquireTokenSilent call
My code is set up as follows:
Program.cs
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetValue<string>("Graph:Scopes")?.Split(' '))
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
builder.Services.AddScoped<IGraphService, GraphService>();
appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com",
"Domain": "{domain}.onmicrosoft.com",
"CallbackPath": "/signin-oidc",
"TenantId": "{tenantId}",
"ClientId": "{clientId}",
"ClientSecret": "{clientSecret}"
},
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read user.readbasic.all"
},
The part of the implementation of the GraphService I created to wrap the GraphServiceClient that tries to set up the GraphServiceClient:
return new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) =>
{
string[] scopes = new[] { "user.read", "user.readbasic.all" };
ITokenAcquisition tokenService = _contextAccessor.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
var token = tokenService.GetAccessTokenForUserAsync(scopes); <-- FAILS!!!
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Result);
return Task.FromResult(0);
}));
Clearing my cookies, this works fine the first time the app runs, but on restarts, it fails with the same message. Injecting the GraphServiceClient directly into a component works fine too, but I'm trying to wrap it so the components are loosely coupled.
Additionally, if I pass the AccessToken from the Blazor Server project to the wrapper library, it works fine too, but I want to avoid that.
What am I missing? Is my setup not right? Thanks for any help or pointers :)
Update 1
To simplify the issue, I have a component on a page and I can successfully request the user's token in that component, every time.
public sealed partial class HeaderBar
{
[Inject] private ITokenAcquisition TokenAcquisition { get; set; } = null!;
[Inject] private IGraphService GraphService { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
try
{
// this works every time!
string token = await TokenAcquisition.GetAccessTokenForUserAsync(new[] { "user.read", "user.readbasic.all" });
var photo = await GraphService.GetUserPhoto();
...
}
catch (Exception ex)
{
...
}
}
}
As you can see, I'm not doing anything with the token, but if I remove that line, the same line within the GraphService fails. It will only succeed and get a token if I make the same call within the component first.
I've tried moving the code to the App.razor, but it fails there too.
Thanks to the following issue, I was able to pinpoint the problem and resolve it:
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/281
Problem:
When you lose the in-memory cache while your cookies are still alive, the auth calls are skipped and you don't bootstrap the token cache, which throws the MsalUIException described in the original question above. That explains why everything works the first time the app runs and doesn't when it gets restarted.
Some of the solutions describe using an attribute to re-populate the cache. However, in Blazor Server, there is no "Controller", so we can't use the AuthorizeForScopes attribute.
Solution:
I've created a page model for the standard _Host.csthml page that will attempt to get a token from the token cache, and if it's not populated, the AuthorizeForScopes attribute will do that for me (Thanks wmgdev!).
[AuthorizeForScopes(Scopes = new[] { "user.read", "user.readbasic.all" })]
public class _Host : PageModel
{
private readonly ITokenAcquisition _tokenAcquisition;
public _Host(ITokenAcquisition tokenAcquisition)
{
_tokenAcquisition = tokenAcquisition;
}
public async Task OnGetAsync()
{
// Get a token. If the token cache is not populated, the `AuthorizeForScopes` attribute will re-authorize, which will populate the cache
await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "user.read", "user.readbasic.all" });
}
}

Swagger UI - authentication only for some endpoints

I am using Swagger in a .NET COre API project.
Is there a way to apply JWT Authentication in Swagger UI only for some endpoints?
I put [Authorize] Attribute only on a few calls (also have tried putting [AllowAnonymous] on the calls that don't need authentication), but when I open the Swagger UI page, the lock symbol is on all the endpoints.
You'll have to create an IOperationFilter to only add the OpenApiSecurityScheme to certain endpoints. How this can be done is described in this blog post (adjusted for .NET Core 3.1, from a comment in the same blog post).
In my case, all endpoints defaults to [Authorize] if not [AllowAnonymous] is explicitly added (also described in the linked blog post). I then create the following implementation of IOperationFilter:
public class SecurityRequirementsOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!context.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) &&
!(context.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ?? false))
{
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "bearer"
}
}, new string[] { }
}
}
};
}
}
}
You'll have to tweak the if statement if you don't default all endpoints to [Authorize].
Finally, where I call services.AddSwaggerGen(options => { ... } (usually in Startup.cs) I have the following line:
options.OperationFilter<SecurityRequirementsOperationFilter>();
Note that the above line will replace the (presumably) existing call to options.AddSecurityRequirement(...) in the same place.

ASP.NET Identity IUserEmailStore error in MVC

I am trying to send a confirmation email when a user registers together with the generated Token. Token is OK. But when sending email,I am getting this error:
System.NotSupportedException: Store does not implement IUserEmailStore<TUser>.
And my code inside AccountController in the Register method looks like this: It is the SendEmailAsync() method which is causing the error:
if (result.Succeeded)
{
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking this link: link");
ViewBag.Link = callbackUrl;
return View("DisplayEmail");
}
In my IdentityConfig.cs file, I have a class:
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
}
Also inside my IdentityConfig.cs file, I have another class:
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
SmtpClient client = new SmtpClient();
return client.SendMailAsync(ConfigurationManager.AppSettings["TranscriptEmailAddr"],
message.Destination,
message.Subject,
message.Body);
}
}
Please assist.
I just figured out that I had Microsoft.AspNet.Identity.EntityFramework v1.0.0 installed but I needed v2.x. I installed it through the Package Manager console by entering:
Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.2.1
In this later version, UserStore implements IUserEmailStore which has the new email methods.
In your model, or somewhere in your code you must have a UserStore class (unless you use a framework like EntityFrameworkIdentity).
Try to find it by searching the entire solution for UserStore and you'll see that you have to implement this interface on it.
like:
public class UserStore : IUserStore<UserIdentity>, IUserPasswordStore<UserIdentity>, IUserEmailStore<UserIdentity>{
}

ASP.net MVC: Accessing Google GoogleWebAuthorizationBroker returns access denied error

I'm trying to upload an video to my YouTube account with the following code in my ActionResult in my asp.net MVC project:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload([Bind(Include = " Title, Description")] HttpPostedFileBase uploadFile, Videos videos)
{
var credential = AuthYouTube();
YouTubeService youtubeService = new YouTubeService(new
YouTubeService.Initializer()
{
ApplicationName = "app-name",
HttpClientInitializer = credential
});
// Do Stuff with the video here.
}}
The AuthYouTube() looks like this (the same controller):
public UserCredential AuthYouTube()
{
string filePath = Server.MapPath("~/Content/YT/client_secret.json");
UserCredential credential;
try{
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
// This OAuth 2.0 access scope allows for full read/write access to the
// authenticated user's account.
new[] { YouTubeService.Scope.Youtube },
"username#domain.com",
CancellationToken.None,
new FileDataStore(Server.MapPath("~/Content/YT"),true)
).Result;
};
return credential;
}
catch(EvaluateException ex)
{
Console.WriteLine(ex.InnerException);
return null;
}
}
I have stored my client_secret.json that I downloaded from Google Developer Console inside the [project]/Content/YT. (Also tried inside the /App_Data folder.
When uploading the debugger is showing the folowwing message:
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.ComponentModel.Win32Exception: Access is denied
Place where the error occures:
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
StackStrace:
[Win32Exception (0x80004005): Access is denied]
Microsoft.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +115
Microsoft.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task) +78
Google.Apis.Auth.OAuth2.<AuthorizeAsync>d__1.MoveNext() in C:\Users\mdril\Documents\GitHub\google-api-dotnet-client\Src\GoogleApis.Auth.DotNet4\OAuth2\GoogleWebAuthorizationBroker.cs:59
[AggregateException: One or more errors occurred.]
System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification) +4472256
Project.Controllers.VideosController.AuthYouTube() in d:\dev\Development\project\project\Controllers\VideosController.cs:133
project.Controllers.VideosController.Upload(HttpPostedFileBase uploadFile, Videos videos) in d:\dev\project\project\Controllers\VideosController.cs:71
What is the reason of this?
- Google API?
- folder / IIS rights?
Update 01-02-2016
Could it be some access error on the API side?
If not, could somebody please provide me the steps to grand the right IIS rights, still get the error after giving folder permissions.
Running the following code DOES create the folder as intended inside my App_Data, but also returns the same 'Access denied' error. The folder is empty.
var path = HttpContext.Current.Server.MapPath("~/App_Data/Drive.Api.Auth.Store");
// here is where we Request the user to give us access, or use the Refresh Token that was previously stored in %AppData%
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets { ClientId = clientId, ClientSecret = clientSecret }
, scopes
, userName
, CancellationToken.None
, new FileDataStore(path,true)).Result;
Could somebody please explain how to get this working?
After ready the documentation again I found a way to get access to the API and upload my videos to YouTube. I hope I can clarify the way i did this.
How i did this:
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-aspnet-mvc
Create an callback controller:
using Google.Apis.Sample.MVC4;
namespace Google.Apis.Sample.MVC4.Controllers
{
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
}
}
Create class and fill-in the credentials:
using System;
using System.Web.Mvc;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.YouTube.v3;
using Google.Apis.Util.Store;
namespace Google.Apis.Sample.MVC4
{
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "PUT_CLIENT_ID_HERE",
ClientSecret = "PUT_CLIENT_SECRET_HERE"
},
Scopes = new[] { YouTubeService.Scope.YoutubeUpload },
DataStore = new FileDataStore(HttpContext.Current.Server.MapPath("~/App_Data/clientsecret.json")),
});
public override string AuthCallback
{
get { return #"/AuthCallback/Upload"; }
}
public override string GetUserId(Controller controller)
{
// In this sample we use the session to store the user identifiers.
// That's not the best practice, because you should have a logic to identify
// a user. You might want to use "OpenID Connect".
// You can read more about the protocol in the following link:
// https://developers.google.com/accounts/docs/OAuth2Login.
var user = controller.Session["user"];
if (user == null)
{
user = Guid.NewGuid();
controller.Session["user"] = user;
}
return user.ToString();
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
}
In my ActionResult I set the YoutubeService. the creating of my video take place inside my Upload POST
Your own controller (mine is for the /upload action):
public async Task<ActionResult> Upload(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var youtubeService = new YouTubeService(new BaseClientService.Initializer
{
HttpClientInitializer = result.Credential,
ApplicationName = "name",
});
return View();
}
else
{
return new RedirectResult(result.RedirectUri);
}
}
For uploading logic see: https://developers.google.com/youtube/v3/code_samples/dotnet#upload_a_video
Set redirect URL in Google Developers console
In the Google Developers Console set the Authorized redirect URIs value to something like (my controller is called videos): http://www.domainname.com/Videos/Upload
**Using a single oAuth account **
Insted of saving the client id (GUID, see GetUserId inside AppFlowMetadata file) inside my session I now use one single id so I could use the same token/responsive for all the users.

How do I convert a refresh token to an access token using the LiveConnect API (C#)

I'm trying to create a LiveConnectClient with only a refresh token that was provided to me via asp.net identity (using OWIN) and the ProviderKey. It looks like the only way to do this without needing HttpContextBase is via InitializeSessionAsync.
When I try and create the client I'm getting:
Microsoft.Live.LiveAuthException: The user ID from the given RefreshTokenInfo instance does not match the refresh token.
Not really sure what user ID it is expecting as I'm giving it the provider key that was passed via ASP.NET Identity (17 chars in my case). Below is my code.
public class Class1
{
protected async Task<LiveConnectClient> GetLiveConnectClient()
{
var authClient = new LiveAuthClient(_clientId, _clientSecret, null, new RefreshTokenHandler(_refreshToken, _providerKey));
var session = await authClient.InitializeSessionAsync("http://.../signin-microsoft");
return new LiveConnectClient(session.Session);
}
}
public class RefreshTokenHandler : IRefreshTokenHandler
{
private readonly string _refreshToken;
private readonly string _userId;
public RefreshTokenHandler(string refreshToken, string userId)
{
_refreshToken = refreshToken;
_userId = userId;
}
public Task<RefreshTokenInfo> RetrieveRefreshTokenAsync()
{
return Task.FromResult(new RefreshTokenInfo(_refreshToken, _userId));
}
public async Task SaveRefreshTokenAsync(RefreshTokenInfo tokenInfo)
{
await Task.Delay(0);
}
}

Resources