I'm trying to authenticate my server app through Google's service account authentication but, for some reason, it is just not pushing through.
In the API console, I already created the project, enabled the service I need (Admin SDK), and created a Service Account and Web Application API Access.
When I do use the web application access credentials I am able to authenticate and retrieve user records. But using service account authentication would keep giving me a login required message.
"error": { "errors": [ { "domain": "global", "reason": "required", "message": "Login Required", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Login Required" }
I forgot to add, I am testing this with the PHP client library.
public function init() {
$client = new Google_Client();
if (isset($_SESSION['access_token'])) {
$client->setAccessToken($_SESSION['access_token']);
}
$key = file_get_contents(App::SERVICE_KEY_FILE);
$client->setAssertionCredentials(new Google_AssertionCredentials(
App::SERVICE_ACCOUNT_NAME,
App::SERVICE_API_SCOPES,
$key)
);
$client->setClientId(App::SERVICE_CLIENT_ID);
debug($client, 'CLIENT');
if ($client->getAccessToken()) {
$this->access_token = $_SESSION['access_token'] = $client->getAccessToken();
debug($_SESSION['access_token'], 'TOKEN');
} else {
debug('NO TOKEN');
}
$this->client = $client;
}
As you can see, the code is basically about the same as the Google example. Am I missing an extra step?
One last thing, when I authenticate using the web app then access my service account script, the service account script can pick up the web app script's session and push through with the user record retrievals. Does that mean the Admin SDK API explicitly needs user interaction through web app authentication?
Instead of service account, I instead opted to use installed applications API Access.
This ruby gem actually helped my figure this out - https://github.com/evendis/gmail_cli
I was playing with it on the console and just followed the authorization steps in the readme, and found that installed applications is more simple when doing server admin apps.
Being a newb, I think I missed the important part the refresh token plays in the entire process. Going via the installed application approach helped me figure that out.
My config file now contains the client id, client secret, api scope, redirect uri, authorization code, and the refresh token; my initialization code now looks like:
public function init() {
$client = new Google_Client();
$client->setClientId(App::CLIENT_ID);
$client->setClientSecret(App::CLIENT_SECRET);
$client->setScopes(App::API_SCOPES);
$client->setRedirectUri(App::REDIRECT_URI);
if (!$client->getAccessToken()) {
$client->refreshToken(App::REFRESH_TOKEN);
}
$this->access_token = $client->getAccessToken();
$this->client = $client;
}
Related
My application is a hybrid approach where use ASP.NET Core MVC as my backend. I have various controllers which my front end uses to pull data from our database and also to do API calls on MS Graph. I am using the following program.cs file to get the authentication initiated when a user first logs on to the site:
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
}).EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
}).AddMicrosoftIdentityUI();
// Add the UI support to handle claims challenges
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddRequiredScopeAuthorization();
In the Azure AD portal my application is registered as a web app. So when a user initially goes to the site they are redirected to https://login.microsoftonline.com/blahblah to get the login process started. This is automated by the Azure AD identity platform. Then once the login occurs they are redirected to localhost where the VueJS spa is loaded (localhost:43862). My spa uses various axios requests to the controllers and they pull data and vue router loads components. However, my issue is say the user needs to relog in because the cookie is expired or they logged out in another tab. The next axios request made by the expired session does not redirect the user to Azure login screen but instead results in an CORS error. So I need to get my axios requests to force the page redirect to Azure AD login screen (which probably is the worst idea since CORS policy is resulting in error) or have it return a redirect to localhost/login which is my own custom login screen with a button to Azure AD login and shouldnt impact CORS. So how do I intercept this Azure AD redirect to Azure AD login and replace with my own?
I have also tried to return a 401 error code so I could check for that in my axios request but to no avail it does nothing. If I put a breakpoint there it does hit this code but it does not change the status code of the response and I still get 302. My code for that was to try and add to the event :
OnRedirectToIdentityProvider = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
My other ideas was maybe I should set my CORS policy to allow redirects from login.microsoft.com? Or would this be bad practice?
I can answer part of your question... First, for our API application which is protected by Azure AD, what the API should do is validating the request whether it contained a correct access token in the request header, if yes, give the response, if no, then give error like 401 or 403. A normal API application shouldn't have a UI to let users sign in. Anyway, if you want to expose an API in an MVC project, it's OK, but for API itself, it shouldn't have a UI.
Let's see sample below, I had a .net 6 web api project, and here's my program.cs:
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
And it requires configurations in appsetting.json.
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "azure_ad_client_id",
"ClientSecret": "client_secret",
"Domain": "tenant_id",
"TenantId": "tenant_id",
//"Audience": "api://azure_ad_client_id_which exposed_api" // here I used the same azure ad app to expose API, so I can comment this property
},
And this is the Controller:
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
[RequiredScope("Tiny.Read")]
[HttpGet]
public string Get()
{
return "world";
}
}
I had an Azure AD app, and I exposed an API like this:
I also add this API for the same Azure AD app.
Then let's do a test. When I call this API directly, I will get 401 error:
If I used an expired token within the request, I will also get 401 error:
But if I used a correct token(go to https://jwt.io to decode the token, we should see it containing correct scope, for me its "scp": "Tiny.Read",), I will get response:
And till now, the API part had finished. Let's see the client SPA. For SPA, you should integrate MSAL so that you can make your users to sign in via Azure AD, and generate the access token for calling MS graph API or your own API. The code for generating access token should be the same but you should set different scope for different API. In my scenario, my API required a scope Tiny.Read, then I should set in my client App.
Here's an screenshot for generating access token in react. You need to set the scope in your code.
Now you have the method to generate access token, you already know the API url. Then you can send request to call api, using AJAX, using fetch, or something else, sending an http request is ok. And in the calling api part, you also need to handle the response. If the response code is 401, then you need to do some logic, maybe redirect to the sign in page. And you said you had trouble here, you met CORS issue. I can't answer this part. I think it depends on how you redirect to Azure AD sign in page. I'm afraid you can take a look at this sample to learn how to sign in users and call graph api.
I have managed to get the GraphServiceClient to work on its own in daemon code, but would like to be able to inject it into my razor pages' constructor as well.
var queryOptions = new List<QueryOption>()
{
new QueryOption("startDateTime", "2022-08-02T16:22:00"),
new QueryOption("endDateTime", "2022-08-02T23:59:00")
};
var res = await _graphServiceClient.Users["smtp>"]
.CalendarView
.Request(queryOptions)
.Select("start,end,subject")
.GetAsync();
What code should be add to the builder?
The Id's and Secrets being read are valid and correct.
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "client.onmicrosoft.com",
"TenantId": "tenantid",
"ClientId": "clientid",
"ClientSecret": "clientsecret"
},
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "https://graph.microsoft.com/.default"
},
I have tried the following and received: MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call even though it exposes an injectable GraphServiceClient to my page.
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
I had the same error when I was trying to call GetAccessTokenForUserAsync (on ITokenAcquisition) at the Api Controller (Asp.net core 6.0).
"Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call."
I was able to resolve it by using AuthorizeForScopes attribute on controller action. Example -
[AuthorizeForScopes(Scopes = new[] { AppConstants.ApiAppScope })]
Where AppConstants.ApiAppScope is the scope of my own WebApi.
If you are calling Graph api, it would be - https://graph.microsoft.com/.default
if you are calling your own Azure AD Web api, it would be something like api://app-id-of-web-api-as-registered-in-Azure-Ad/scope-defined-under-expose-as-api (i.e. api://ff1acf30-8c34-45ce-9bf6-7a47fa42d6be/access_as_user)
I couldn't figure out how to use built-in InMemory cache and I ended up creating my own caching. Caching is needed because call to GetAccessTokenForUserAsync will prompt the user to login again (if the scope is new).
This is my own application (self-disclaimer) that I am using for research (working sample)- https://securityresearchlab.aspnet4you.com/
We've started implementing a new Web API with ASP.NET Core 2.2 and it has been decided that it should use Identity Server 4 for authentication/authorization duties. Furthermore, its Skoruba implementation has been chosen as it looks like it should fulfill most, if not all our needs in that regard.
We got the identity server and Skoruba up and running, but when it comes to consuming the JWT token in our own API, even assigning just one role to the test user, we'll keep hitting the same brick wall. The following request to the Skoruba API will respond with a 200:
http://localhost:5000/connect/token:
It successfully returns a JSON string with access_token, expires_in and token_type ("Bearer").
After that, a request to the http://localhost:5000/connect/userinfo route of the API with the following header
will also respond with a 200 and return the following JSON string:
{
"sub": "aeccf460-7d0d-41ae-8b52-a051138f5c05",
"role": "Administrator",
"preferred_username": "dev",
"name": "dev"
}
Please take notice that "role": "Administrator" assigned to user dev is something I set up myself using the Skoruba Admin UI, so that JSON is pretty much what I wanted. So for all intended purposes it looks like I have the information I need right now. I just can't consume it. If I try to retrieve the JWT token in our own back end, I am successful (this is obviously just for testing purposes):
[HttpGet("GetAccessToken")]
[AllowAnonymous]
public string GetAccessToken()
{
var accessToken = HttpContext.Request.Headers["Authorization"];
var token = accessToken.First().Remove(0, "Bearer ".Length);
return token;
}
With all that said, onto the actual problem: calls to a route that demands authorization in our API are treated in the same fashion as calls to Skoruba's userinfo action (particularly, the headers):
Inside this same Controller ("Foo"), I implemented a simple Get method, which should only be accessed with the correct role, which I assume is information fetched from HttpContext.Request.Headers["Authorization"] and hoped the framework would use it accordingly:
[HttpGet]
[Authorize(Roles = "Administrator")]
public IActionResult Get()
{
try
{
var response = ConvertListToJsonResponse(GetAll()); //Don't mind me
return Ok(response);
}
//...
}
At this point, my API server responds with the infamous 403 Forbidden status code. Not sure where to go from here and research proved unwieldy so far. Any help is appreciated, I'll provide more code and info if necessary.
EDIT
This is the generated token:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4NTMzNmFmZTY0Yzg2ZWQ3NDU5YzE5YzQ4ZjQzNzI3IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1Njg3NDU5NTgsImV4cCI6MTU2ODc0OTU1OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwL3Jlc291cmNlcyIsImNsaWVudF9pZCI6ImF1dGhfdGVzdCIsImNsaWVudF9hY2Nlc3NfcmlnaHRzIjpbImxpc3QiLCJhcHByb3ZlIiwia2VlcCJdLCJzdWIiOiJhZWNjZjQ2MC03ZDBkLTQxYWUtOGI1Mi1hMDUxMTM4ZjVjMDUiLCJhdXRoX3RpbWUiOjE1Njg3NDU5NTgsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsInJvbGVzIl0sImFtciI6WyJwd2QiXX0.Ihsi6W5ukGcZ4Chkuk5XMaoqTkUR_1hBQlIcdHtMWiBA-EyAIncX5STCng_6ZPgN89Np6Y_hemFFyVtHEdN_vP6i0HuaXgznzrnJ4zq4Iiz9jxpZqpSSE9cXpSG8hPOZe5kGfD2J6_GPxnraGH_1ZF94AhmlspIvqFAAQrQ-0c7-dCduP4ledkQvBKz-rXszGp35W7Gb5nvpcVt4oe67mqETdwtgGengk2eCwHeKdA94EYnj_HErPNTjJhh5k75fDQ0IiOS-xHRK8BQmLhRh_UZwB3H5qZymFJNr_yb-ljFqIeEHptSWLBO1XrKYs1BqB9KwxIROKqmxeNGTnpCUSQ
The resulting payload:
{
"nbf": 1568745958,
"exp": 1568749558,
"iss": "http://localhost:5000",
"aud": "http://localhost:5000/resources",
"client_id": "auth_test",
"client_access_rights": [
"list",
"approve",
"keep"
],
"sub": "aeccf460-7d0d-41ae-8b52-a051138f5c05",
"auth_time": 1568745958,
"idp": "local",
"scope": [
"openid",
"profile",
"roles"
],
"amr": [
"pwd"
]
}
I see the claims I added to the client, but what I really need at the moment is simple Authentication, which I suppose should be provided by a role. Or am I completely off?
This question, and more specifically, this answer helped me to understand what was going on and map Skoruba's UI functionalities to IdentityServer4 inner workings. Credits goes to Ruard van Elburg.
I've got URI endpoint authentication working for both facebook & google in my express app through separate middlewares. Facebook uses passport facebook-token strategy, whereas for google I wrote my own middleware using nodejs client lib for google API. What I want is to authenticate a user on a URI endpoint using both these middleware.
/*
//google controller file
module.exports = function(req,res,next){
}
*/
googlectrl = require('google controller file');
//this works fine
app.get('someurl',googlectrl,function(req,res){
//google user authenticated
}
//this works fine too
app.get('someurl',passport.authenticate('facebook-token',{session=false}),function(req,res){
//google user authenticated
}
But how do I combine the two for the same uri. Otherwise I need to use seperate URI for google & fb. Pls advice. Pls note I've tried implementing google strategy and it has not worked.
You can use one array field for your user object named as providers as shown below:
{
"name": "asdasd",
"providers": [ 'google']
}
And at server side check user is using which provider and call the method accordingly. For example:
If a user requests with google service provider then call
function handleGoogleAuthentication(){
// Logic
}
And if a user requests with facebook service provider then call
function handleFacebookAuthentication(){
//Logic
}
try
{
UserCredential credential;
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets { ClientId = ClientID, ClientSecret = ClientSecret },
new[] { AnalyticsService.Scope.AnalyticsReadonly, AnalyticsService.Scope.AnalyticsEdit },
"user",
CancellationToken.None,
new FileDataStore("Analytics.Auth.Store")).Result;
return credential;
}
catch { return null; }
I am using above code for google console web application(Google Analytic) but it gives redirect_uri mismatch error. How i can send redirect_uri.
redirect_uri is set up in the Google Developers console -> apis & auth -> credentials
Not sure if Sanaan C ever found an answer ... the reason that your code does not work in a web application is likely because the user that created the Analytics.Auth.Store entry in that user's %APPDATA% folder is NOT the one running your web application.
Does anyone have a solution to this - and please excuse that this question is appended to another ... I actually think this was the intended question in any event ...
===
One simple-minded solution could be to take the credentials created by a user who can respond to the redirect and put it in a folder, with appropriate access permissions, where the user under which the IIS service is being run can find it. Instantiate the FileDataStore with a full path to this folder ...