I have an app that will operate almost entirely on Spotify OAuth, that will have features to alter the playback of your music.
I'm able to get Spotify OAuth working perfectly such that I can log into my app, but after I've logged in, I need access to the current user's spotify access_token so that I can forward it to my spotify requests.
I followed this guide from ms to try to save the tokens: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/?view=aspnetcore-6.0&tabs=visual-studio
And I have tried all these ways to then save that token into the HttpContext such that I can access it:
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
var spotifyAccessToken = tokens.FirstOrDefault(x => x.Name == "access_token").Value;
tokens.Add(new AuthenticationToken()
{
Name = "SpofityAccessToken",
Value = spotifyAccessToken
});
//store all the tokens as directed by MS
ctx.Properties.StoreTokens(tokens);
//store the properties into the HttpContext in 2 different ways
ctx.HttpContext.Items["Properties"] = ctx.Properties;
ctx.HttpContext.Features.Set(ctx.Properties);
//try adding a claim to the user
ctx.Identity.AddClaims(new[] { new Claim("SpotifyAccessToken", spotifyAccessToken) });
return Task.CompletedTask;
};
The problem I'm having is how do I then get this token out? all of these methods are not working:
[HttpGet]
public async Task Get()
{
await HttpContext.SignInAsync(User);
// try to re-run Authenticate, even though i'm already in an [Authorize] controller
var res = await HttpContext.AuthenticateAsync();
//props2 does not have the tokens i set
var props2 = res.Properties;
//props comes back null
var props = HttpContext.Features.Get<AuthenticationProperties>();
//claims has no SpotifyAccessToken claim
var claims = User.Claims.ToList();
var token = "hard-coded";
//here is where i need the token to then query spotify
var client = new SpotifyAPI.Web.SpotifyClient(token);
var res2 = await client.Player.GetCurrentPlayback();
}
I feel like I've tried everything, what am i doing wrong?
This is in a .NET 6 blazor wasm, .net core hosted app.
Also tried the solutions here to no avail Get AuthenticationProperties in current HttpRequest after HttpContext.SignInAsync
signInManager.UpdateExternalAuthenticationTokensAsync adds the the authentication tokens in [dbo].[AspNetUserTokens]
External login is where I call it:
// Sign in the user with this external login provider if the user already has a login.
var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true);
if (signInResult.Succeeded)
{
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
Later on you can get it by using :
var token = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "access_token");
var expiresAtStr = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "expires_at");
If the token is stored in the Cookie then you can access the various tokens using:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");
string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
string tokenType = await HttpContext.GetTokenAsync("token_type");
string accessTokenExpire = await HttpContext.GetTokenAsync("expires_at");
However, you can not store data in ctx.HttpContext and assume it will be persisted across requests. either you sign-in the user using the cookie middleware or you store the tokens in the UserSession object.
See this article on how to configure and store data in the session, that will be persisted across requests.
Session and state management in ASP.NET Core
If you configure it properly, then you can use it like:
HttpContext.Session.SetString("token", token.Trim());
Related
I've configured External provider authentication to my Blazor WASM app. User can choose to log in via Spotify account and after that, I want my server to download some data about him from Spotify API.
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signin-spotify";
options.SaveTokens = true;
var scopes = new List<string> {
//scopes
};
options.Scope.Add(string.Join(",", scopes));
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
ctx.Properties.IsPersistent = true;
return Task.CompletedTask;
};
});
In order to call Spotify API, I need an access token. Token is, if I understand correctly, given to my server after user logs in. In above code, I've specified OnCreatingTicket event and I can see it is being fired (just after I log in) and access_token is in tokens list.
Now, the problem is, I don't know how to retrieve that token later.
Here is what happens after log in:
User navigates to \LikedSongs (blazor wasm subpage that is meant to display data)
Blazor page calls my server's API to retrieve data that will be later displayed
protected override async Task OnInitializedAsync()
{
savedTracks = await HttpClient.GetFromJsonAsync<SavedTrack[]>("UsersTracks");
}
Finally, my API controller is being fired:
[HttpGet]
public async Task<IEnumerable<SavedTrack>> GetAsync()
{
// here I need to have access_token
// ASP.net MVC tutorial I follow states, that below line should work
var token = await _httpContextAccessor.HttpContext.GetTokenAsync("Spotify", "access_token");
// unfortunately token == null
}
For some reason, token is null. And I can't find any other tokens in HttpContext. As I understand correctly, tokens are encoded in cookies, so why I can't find any of them there?
I have a WebApi2 app which servers as api for my app frontend. Now i want to use AD B2C to manage my users - let's say I want to differentiate them by their roles (admin or customer) and for that i created two b2c users groups accordingly. When user logs in i want to display different things for users with different roles (groups).
I'm using this example to setup Startup.Auth.cs in my WebApi2 project:
var tvps = new TokenValidationParameters
{
ValidAudience = clientId,
AuthenticationType = signUpSignInPolicy,
};
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(String.Format(aadInstance, tenant, defaultPolicy))),
});
From what I have read b2c doesn't return user's grups in claims for now. Some people suggested I need to call GraphApi after obtaining token to fetch these groups and add them to user's claims:
private static async Task<string> GetGroups(string token, string userId)
{
using (var client = new HttpClient())
{
var requestUrl = $"https://graph.microsoft.com/v1.0/users/{userId}/memberOf?$select=displayName";
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
}
This is where I've stuck. How can I inject my code to get token for calling graph? I've messed with OAuthBearerAuthenticationOptions.Provider:
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = (context) =>
{
// var token = ??
// var userId = <get from context's claims>
// var groups = GetGroups(token, userId);
// <add to claims>
return Task.CompletedTask;
}
},
...but I don't know how to get to token. Maybe that's wrong from the start and I need another approach?
Customer's token cannot be used to call AADGraph/MSGraph Apis. To get token to call graph apis in an automated way, we need app-only access. We need to configre an app in the tenant, the crendetial of which are used to get a token. That token can then be used to call memberOF Api (or any other api which does or require user information to be there)
Here is the sample and explaination of how to call AAD Graph apis in a B2C dependent service.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet
I'm currently confused about how to realize the authentication / authorization flow.
I'm developing two applications, the one is the frontend/Webapplication and the other the backend/API, both with ASP.NET Core. The goal is to use the AzureAD and use the users/groups from the domain. The authentication I already implemented on both applications and I'm able to login and restrict content based on the login state.
As reference I took this example from a microsoft developer. There should be exactly this what I want to do. There is a WebApp and API. The used authentication flow is the authorization code flow. First the user needs to login and after that when some data needs to be requested from the API, an access token will be requested.
Question 1: Is this the right authentication flow? For me this seems like a doubled authentication, because first I authenticate myself at the frontend and when the Webapp needs some data I need to authenticate myself again at the backend. The same Azure AD tenant is used, so what do you think here?
The next point what seems very "ugly" is the procedure getting some data. In the example when some data is requested first the token will be requested and after this the data. But in my opinion with a lot of boilerplate. The example code below is needed for just one request of all ToDo items.
// GET: /<controller>/
public async Task<IActionResult> Index()
{
AuthenticationResult result = null;
List<TodoItem> itemList = new List<TodoItem>();
try
{
// Because we signed-in already in the WebApp, the userObjectId is know
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
// Using ADAL.Net, get a bearer token to access the TodoListService
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
result = await authContext.AcquireTokenSilentAsync(AzureAdOptions.Settings.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
// Retrieve the user's To Do List.
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AzureAdOptions.Settings.TodoListBaseAddress + "/api/todolist");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Return the To Do List in the view.
if (response.IsSuccessStatusCode)
{
List<Dictionary<String, String>> responseElements = new List<Dictionary<String, String>>();
JsonSerializerSettings settings = new JsonSerializerSettings();
String responseString = await response.Content.ReadAsStringAsync();
responseElements = JsonConvert.DeserializeObject<List<Dictionary<String, String>>>(responseString, settings);
foreach (Dictionary<String, String> responseElement in responseElements)
{
TodoItem newItem = new TodoItem();
newItem.Title = responseElement["title"];
newItem.Owner = responseElement["owner"];
itemList.Add(newItem);
}
return View(itemList);
}
//
// If the call failed with access denied, then drop the current access token from the cache,
// and show the user an error indicating they might need to sign-in again.
//
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return ProcessUnauthorized(itemList, authContext);
}
}
catch (Exception)
{
if (HttpContext.Request.Query["reauth"] == "True")
{
//
// Send an OpenID Connect sign-in request to get a new set of tokens.
// If the user still has a valid session with Azure AD, they will not be prompted for their credentials.
// The OpenID Connect middleware will return to this controller after the sign-in response has been handled.
//
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme);
}
//
// The user needs to re-authorize. Show them a message to that effect.
//
TodoItem newItem = new TodoItem();
newItem.Title = "(Sign-in required to view to do list.)";
itemList.Add(newItem);
ViewBag.ErrorMessage = "AuthorizationRequired";
return View(itemList);
}
//
// If the call failed for any other reason, show the user an error.
//
return View("Error");
}
Question 2: Is there a "less ugly" approach to access the data if the flow in Q1 is right?
I found a proper solution to solve this.
I just used the approach from this example here multitenant-saas-guidance and it works like charm.
I'm trying to create an ASP.NET application with Stormpath ID Site authorization. I create request and response action and successfully got the account.
But what next? How to tell the application that the user is authenticated?
public async Task<RedirectResult> Callback(string jwtResponse)
{
var client = Request.GetStormpathClient();
var app = await client.GetApplicationAsync(appUrl);
var requestDescriptor = HttpRequests.NewRequestDescriptor()
.WithMethod("GET")
.WithUri("http://localhost:50084/Auth/Callback?jwtResponse=" + jwtResponse)
.Build();
var idSiteListener = app.NewIdSiteAsyncCallbackHandler(requestDescriptor);
var accountResult = await idSiteListener.GetAccountResultAsync();
var account = accountResult.GetAccountAsync().Result; //Account
//What I must do here to tell application that user is authenticated
return Redirect("/");
}
Instead of getting the account from the ID Site response, you could exchange the JWT for a Stormpath access token:
public async Task<RedirectResult> Callback(string jwtResponse)
{
var client = Request.GetStormpathClient();
var app = await client.GetApplicationAsync(appUrl);
var exchangeRequest = new StormpathTokenGrantRequest
{
Token = jwtResponse
});
var grantResponse = await application.ExecuteOauthRequestAsync(exchangeRequest);
// Return grantResponse.AccessTokenString in a secure HTTPOnly cookie, or as a JSON response
}
If you use the Stormpath ASP.NET plugin, you can enable ID Site and this will be handled for you automatically.
Disclaimer: I'm the package author.
I am new to OAUTH. I have been working on implementing OAUTH into my MVC c# application to access ping federate. After much research, and failed attempt at using the ping federate nuget, I came across this link that finally gave some clarity to the full process with a coding example. I have came across much generic examples of the endpoints i need to access but never a full workflow coding example. After implementing that code with some changes and was successful at signing in the ping user into my MVC app, I started doing more research about the refresh token. Questions...
Q. I know how to access a a refresh token, meaning I know which endpoint used to refresh the access token after I have authenticated the user in ping federate. But what is the refresh token used for? Is it used to extend my application's session once it ends? Or it used for if the user signs out of my application then they click the 'Sign in with Ping Federate' link on the login and not have them authenticate again as long as the refresh token is still valid?
Q. And if the refresh token is used for when after a user authenticates the first time, and I save the refresh token in the db and then user signs back using that 'Sign in with Ping Federate' link on my login back how can I know what user that is to lookup the refresh token in the db to give them access to my site without re-authenticating them with ping federate? Since when they come to that link 'Sign in with Ping Federate' I do not know who they are?
This is the below code that I am using, from user MatthiasRamp in the link i provided...I want to add my refresh token logic with the below code.
public async Task<ActionResult> Login(string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl) && Request.UrlReferrer != null)
returnUrl = Server.UrlEncode(Request.UrlReferrer.PathAndQuery);
if (Url.IsLocalUrl(returnUrl) && !string.IsNullOrEmpty(returnUrl))
_returnUrl = returnUrl;
//callback function
_redirectUrl = Url.Action("AuthorizationCodeCallback", "ExternalLogin", null, Request.Url.Scheme);
Dictionary<string, string> authorizeArgs = null;
authorizeArgs = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"response_type", "code"}
,{"scope", "read"}
,{"redirect_uri", _redirectUrl}
// optional: state
};
var content = new FormUrlEncodedContent(authorizeArgs);
var contentAsString = await content.ReadAsStringAsync();
return Redirect("http://localhost:64426/oauth/authorize?" + contentAsString);}
public async Task<ActionResult> AuthorizationCodeCallback()
{
// received authorization code from authorization server
string[] codes = Request.Params.GetValues("code");
var authorizationCode = "";
if (codes.Length > 0)
authorizationCode = codes[0];
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"client_secret", "ClientSecret"}
,{"grant_type", "authorization_code"}
,{"code", authorizationCode}
,{"redirect_uri", _redirectUrl}
};
var client = new HttpClient();
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync("http://localhost:64426/token", postContent);
var content = await response.Content.ReadAsStringAsync();
// received tokens from authorization server
var json = JObject.Parse(content);
_accessToken = json["access_token"].ToString();
_authorizationScheme = json["token_type"].ToString();
_expiresIn = json["expires_in"].ToString();
if (json["refresh_token"] != null)
_refreshToken = json["refresh_token"].ToString();
//SignIn with Token, SignOut and create new identity for SignIn
Request.Headers.Add("Authorization", _authorizationScheme + " " + _accessToken);
var ctx = Request.GetOwinContext();
var authenticateResult = await ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalBearer);
ctx.Authentication.SignOut(DefaultAuthenticationTypes.ExternalBearer);
var applicationCookieIdentity = new ClaimsIdentity(authenticateResult.Identity.Claims, DefaultAuthenticationTypes.ApplicationCookie);
ctx.Authentication.SignIn(applicationCookieIdentity);
var ctxUser = ctx.Authentication.User;
var user = Request.RequestContext.HttpContext.User;
//redirect back to the view which required authentication
string decodedUrl = "";
if (!string.IsNullOrEmpty(_returnUrl))
decodedUrl = Server.UrlDecode(_returnUrl);
if (Url.IsLocalUrl(decodedUrl))
return Redirect(decodedUrl);
else
return RedirectToAction("Index", "Home");
}