How to do 3-legged OAuth login with One Tap sign-in on Android - google-oauth

I'm trying to implement 3-legged OAuth login with Google's One Tap sign-in for Android. This is needed to access Gmail from the server side.
The app needs to get a server auth code and pass it to the server which stores it. The server will later exchange it for a refresh token and access token and use it to pull email.
I use the following code (error checking removed for brevity) which gets me a token, but it's NOT a refresh token. What I need is a server auth code that I can exchange for access and refresh tokens like.
// Show the one tap sign in account selector
List<String> scopes = new ArrayList<>(List.of("https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email",
..., ... // other scopes come here
));
BeginSignInRequest signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.associateLinkedAccounts(LINKED_SERVICE, scopes) // what is this LINKED_SERVICE?
.setSupported(true)
.setServerClientId(SERVER_CLIENT_ID)
.setFilterByAuthorizedAccounts(false)
.build())
.setAutoSelectEnabled(false)
.build();
SignInClient oneTapClient = Identity.getSignInClient(activity);
oneTapClient.beginSignIn(signInRequest).addOnCompleteListener(new OnCompleteListener<BeginSignInResult>() {
#Override
public void onComplete(#NonNull Task<BeginSignInResult> task) {
if (!task.isSuccessful())
return;
BeginSignInResult signInResult = task.getResult();
PendingIntent signInIntent = signInResult.getPendingIntent();
activity.startIntentSenderForResult(signInIntent.getIntentSender(), REQ_SIGN_IN, null, 0, 0, 0));
}
});
Then in the intent receiver:
// Handle response from the one tap account selector
public void onResult(Activity activity, int requestCode, int resultCode, Intent data) {
// Assume requestCode==REQ_SIGN_IN and resultCode is OK (for brevity)
SignInCredential cred = oneTapClient.getSignInCredentialFromIntent(data);
String idTokenString = cred.getGoogleIdToken();
// Now what? This is an Oauth token for client-side use.
// How to I get a server auth code that can be exchanged for a refresh and access tokens at the server-side?
}
Is there a way to get the server auth code?
Comment:
Until now I was using the GoogleSignIn API in the following manner, but it's being phased out and not sure when it's going to stop working (documentation states March 31, 2023).
// Start the OAuth sign in
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(new Scope("https://www.googleapis.com/auth/userinfo.profile"), new Scope("https://www.googleapis.com/auth/userinfo.email"))
.requestServerAuthCode(SERVER_CLIENT_ID)
.requestEmail()
.build();
Intent intent = GoogleSignIn.getClient(activity, gso).getSignInIntent();
activity.startActivityForResult(intent, REQ_SIGN_IN);
The result is returned to the activity (again, no error checking)
public void onResult(Activity activity, int requestCode, int resultCode, Intent data) {
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
GoogleSignInAccount acct = task.getResult();
String authCode = acct.getServerAuthCode(); // Server will exchange this for access and refresh tokens
// Pass this authCode to the server
}

New Google Sign-In API using Identity.getSignInClient(activity) doesn't support 3-legged OAuth.
The documentation states:
For new apps we recommend using Google Identity Services instead of the
Google Sign-In API for sign-in and sign-up, unless you need
authorization, Server-Side Access (!!!), or custom OAuth scopes.
Old Google Sign-in is scheduled to be deprecated at March 31, 2023. However, until now (September 2022) there is no sign that the new API will add support.

Linked account sign-in uses access and refresh tokens issued by another, non-Google partner site or platform. Data is shared from the partner platform TO Google using OAuth2. In this scenario the partner platform defines the scope names, and Google requests and requests and manages the access and refresh tokens issued by the partner platform -- your app does not manage the
credentials. An example of this might be playing music or video managed by a different company on a Google Home hub.
From your comments it sounds like you are looking to request Google scopes and work with Google Account user data? In your example code, you are using authentication scopes only (email, profile) and simply using One Tap to obtain an ID token would be the most straightforward. Should you need to work with Google scopes, for Android you'll currently need to use the older Google Sign-in APIs. Somewhat confusingly, the sign-in options for user authentication are using newer Android API's while authorization to work with Google Account user data remains on the older Google Sign-in APIs.

Related

B2C Authenticate User In Web Api

I have a web app where a user logs in through b2c and have certain claims set once they're logged in. Now, I also need to call an api with javascript when a user clicks a button. So that's all fine but, here's my question:
How would I get the user's claims in the api?
My api and client side web app are both in asp.net core.
Thanks in advance!
At first, you may refer to this document or this official sample to check if it met your requirement.
For the api, it will return user information which is signed in the app.
[HttpGet]
public IEnumerable<Todo> Get()
{
string owner = User.Identity.Name;
return TodoStore.Values.Where(x => x.Owner == owner);
}

What's the best way of web api authentication to Microsoft Graph to access emails in the service account mailbox?

I am trying to figure out which way would be better for my application. I need to perform an automatic email import triggered from ASP.NET Core Web API using Microsoft Graph mailbox access. According to the documentation, there are two options to go for:
Get access on behalf of the user (https://learn.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0)
Get access without the user (https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0)
As the import is automatically triggered by Azure Function timer I do not want to open a popup for the user credentials. So I considered to go with the second option and create a service user to do this, but then I saw a point about Admin consent in the documentation and I got a bit confused. Does this mean that if nobody is going to accept the app rights to access emails it will not be able to do so? What would be easier/preferred way to implement this kind of functionality?
--Does this mean that if nobody is going to accept the app rights to access emails it will not be able to do so?
no, that means after admin consent the api permission, then you can access any users' emails in your tenant.
--What would be easier/preferred way to implement this kind of functionality?
as you said that you are using Azure timer function to to auto import the mails, so you shouldn't get access to graph api on behalf of a user, you have to access the api on behalf the application.
But you have to check if the graph api you want to call support being accessed on behalf of the application. Let's see this api. You may notice that it supports Application api permission.
Then pls note that you have to using client credential flow to generate access token to call the graph api, you can't use other flows. Here's sample code for using client credential flow with graph SDK to call graph api.
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "azure_ad_appid";
var clientSecret = "client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var inboxMessages = await graphClient.Users["user_id"]
.MailFolders["inbox"]
.Messages.Request().GetAsync();

How to authenticate on behalf of user to access their google calendar information from webjob/azure function?

We have a webapp that will allow users to sync certain events in their calendar to our system. We can have the user login to google and authorize us to read their calendar and profile.
Now in order to sync their events we want to have an azure function, webjob running at certain intervals that connects to their google calendar and based on some logic will add the event information to our db so that the user can view content in our web app.
How can I get the azure function/webjob to authenticate on the users behalf after they have enabled the featured and authorized out app to get access to their calendar?
=======================================
UPDATE:
This is the code snippet example if anyone else needs it.
TokenResponse token = new TokenResponse { RefreshToken = syncUser.GoogleRefreshToken };
UserCredential credentials = new UserCredential(new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "ClientIDValue",
ClientSecret = "ClientSecretValue"
}
}), "user", token);
CalendarListResource.ListRequest request = service.CalendarList.List();
CalendarList calendars = request.Execute();
Assuming you are storing the users refresh tokens and you can have your azure function read from where ever it is you are storing the refresh token. You just need to use the refresh token to request a new access token for the user and you can access their data offline.
Now I am not an Azure expert, but your going to have to ensure that you set up your redirect uri properly so that azure can get the response back from the authorization. You should also set your code to handle the refresh token expiring.
Also GoogleAuthorizationCodeFlow.Initializer supports IDataStore which means you could set up your own implementation of Idatastore and use that to feed it the refresh token from where ever it is you are grabbing it from.

OAuth Implicit flow Access Token expires every hour

I'm having a problem with the OAuth Implicit flow for the Google Assistant.
I managed to set up a OAuth server and got it to work. Here's the flow:
The user Is redirected to my endpoint, authenticates with a Google account, and gets send back to the Assistant with an Acces Token and result code=SUCCES.
In my fullfilment I get the users email address by doing a https request to: https://www.googleapis.com/plus/v1/people/me?access_token=access_token.
I then find the matching user in my database and add the acces token to the database for this user.
The next time the user logs in I check the acces token and greet the user by their name.
Now the problem is that this is the Implict flow which according to the documentation should have an access token that never expires:
Note: Google requires that access tokens issued using the implicit
flow never expire, so you don't need to record the grant time of an
access token, as you would with other OAuth 2.0 flows.
But the Assistant forces me to re-authenticate every hour, meaning the access token did expire.
My question is: Is this flow correct or am I missing something? Is there something I've done wrong in my OAuth endpoint?
I based my endpoint on https://developers.google.com/identity/protocols/OAuth2UserAgent.
<html>
<head>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<meta name="google-signin-client_id" content="CLIENT_ID">
</head>
<body>
<script>
var YOUR_CLIENT_ID = 'CLIENT_ID';
function oauth2SignIn() {
// Google's OAuth 2.0 endpoint for requesting an access token
var oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
// Create element to open OAuth 2.0 endpoint in new window.
var form = document.createElement('form');
form.setAttribute('method', 'GET'); // Send as a GET request.
form.setAttribute('action', oauth2Endpoint);
//Get the state and redirect_uri parameters from the request
var searchParams = new URLSearchParams(window.location.search);
var state = searchParams.get("state");
var redirect_uri = searchParams.get("redirect_uri");
//var client_id = searchParams.get("client_id");
// Parameters to pass to OAuth 2.0 endpoint.
var params = {
'client_id': YOUR_CLIENT_ID,
'redirect_uri': redirect_uri,
'scope': 'email',
'state': state,
'response_type': 'token',
'include_granted_scopes': 'true'
};
// Add form parameters as hidden input values.
for (var p in params) {
var input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', p);
input.setAttribute('value', params[p]);
form.appendChild(input);
}
// Add form to page and submit it to open the OAuth 2.0 endpoint.
document.body.appendChild(form);
form.submit();
}
oauth2SignIn();
</script>
</body>
</html>
It sounds like what you are doing is having the user log into your page, and using this to get an auth token from a Google service. You're then turning this around and passing this back to the Assistant and calling this the Identity Flow.
While clever - this isn't the Identity Flow.
This is you using the Auth Code Flow to authenticate the user with Google, and then returning this token to Google and pretending this is an Identity Flow token. However, since you're using the Auth Code Flow, the auth tokens that you get back expire after an hour. (You can check out the lifetime in the information you get back from Google.)
If you are trying to do Account Linking and not manage anything yourself, you need to actually implement an OAuth server that proxies the Auth Code Flow requests from the Assistant to Google and the replies from Google back to the Assistant. While doable, this may be in violation of their policy, and isn't generally advised anyway.
Update to address some questions/issues in your comment.
using the Google Auth endpoints doesn't store the session either, so you'd still have to re-authenticate every hour
Since the Google Auth endpoints use the Auth Code Flow, you can use the offline mode to request a refresh token. Then, when an auth token expires, you can use the refresh token to get a new auth token. So you still have a long-term authorization for access and can get the short-term token to do the work you need.
Trying to shoehorn this into the Identity Flow, however, doesn't work. (And would be a really bad idea, even if it did.)
Can you provide some clarification on how to create an endpoint for the implicit flow?
Beyond the step-by-step description of what your OAuth server code can do in the Assistant documentation, I'm not sure what clarification you need. Your OAuth server fundamentally just needs to:
Be able to have a user:
Connect to an HTTPS URL
Authenticate themselves
Authorize the Assistant to contact your service on their behalf
Return a code by redirecting the user to Google's URL with a code in the parameter
And the Action webhook needs to be able to:
Accept this code as part of the request from the Assistant and
Figure out who the user is from this code. (ie - map the code to a user account in your system.)
There are a variety of ways you can do all of that. The OAuth server and Action could be on the same server or separate, but they at least need to have some agreement about what that code is and how that maps to your user accounts.
If your primary need is to access Google APIs on behalf of your user - then the user account that you have will likely store the OAuth tokens that you use to access Google's server. But you should logically think of that as separate from the code that the Assistant uses to access your server.
(As an aside - those steps are for the Identity Flow. The Auth Code Flow has a few more steps, but the fundamentals are similar. Especially on the Action side.)

OpenID Connect server. Generate access token based on 3-rd party token (social login)

I had implemented OpenID Connect server that generates access tokens for mobile client based on username/password using OpenIddict.
My next goal was to provide ability to generate Access Token using 3-rd party tokens (social login for example), and I started from integration with Google token, but stuck as cannot find any samples/informations about how to do this.
The only one idea that I currently have is to make request to "/connect/token" endpoint and send Google token in "code" parameter, for example in "google:" format, then override OpenIdConnectServerProvider.DeserializeAuthorizationCode method:
Called when receiving an authorization code. An application may use this context to deserialize the code using a custom format and to skip the default logic using
So I have created own CustomProvider class based on OpenIddictProvider, registered it
services.AddOpenIddict<ApplicationUser, ApplicationRole, ApplicationDbContext, int>()
.Configure(builder =>
{ builder.Provider = new CustomProvider(sp.GetRequiredService<SignInService>()); }
and overridden the DeserializeAuthorizationCode method:
public override async Task DeserializeAuthorizationCode(DeserializeAuthorizationCodeContext context)
{
string code = context.Request.Code;
if (code.StartsWith("google:"))
{
string token = code.Replace("google:", "");
var principal = new GoogleTokenValidator().ValidateToken(token, null).Result;
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), "Bearer");
ticket.SetPresenters(context.Request.ClientId);
context.Ticket = ticket;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(1);
context.HandleResponse();
await _signInService.Login(principal);
return;
}
else
{
base.DeserializeAuthorizationCode(context);
}
}
where GoogleTokenValidator is a custom class for Google token handling (it makes call to Google User Information Endpoint and generate ClaimsPrincipal), based on "copy-pasted" code from GoogleHandler class in aspnet/Security repo.
In general it is working with some additional hacks, but I have strong feeling that reinventing the wheel...
In general it is working with some additional hacks, but I have strong feeling that reinventing the wheel...
You're not only reinventing the wheel, but you're also implementing something totally non-standard that is not supported (at all) by OpenIddict.
Here's the approach I recommend (which is the one we use in the MVC server sample):
The OAuth2/OpenID Connect client application redirects the user agent to your authorization controller (you can take a look at this controller for an example).
OpenIddict will validate the authorization request and allow your controller to be invoked if it's fully valid.
If the user is not already logged in, your authorization controller will redirect the user to the login endpoint, provided by AccountController. At this point, you're free to propose local authentication (e.g using a username/password couple) or Google authentication (you can use the Google authentication middleware for that). You can even offer 2-FA as part of this login process.
Once the user is logged in (e.g after a registration process and/or an external authentication association), his/her browser is redirected back to the authorization endpoint, where a consent form indicating he/she's about to allow your JS app to access his personal data on his/her behalf is displayed.
When the user allows your client application to access his data, the request is handled by your authorization controller, that calls SignInAsync to inform OpenIddict that an authorization code/access token should be returned to your application.