How to receive user photos in MS Teams Bot which installed in different organizations? - azure-active-directory

According to the documentation https://learn.microsoft.com/en-us/graph/api/profilephoto-get?view=graph-rest-beta - in order to get the user photo we need to perform HTTP request https://graph.microsoft.com/beta/users/{userId}/photo/$value providing the access token and user id.
Using the client credentials flow I can receive the access token for a specifiс organization (tenant id), for example:
public static String getAppAccessToken(String[] scopes) {
ConfidentialClientApplication cca;
try {
cca = ConfidentialClientApplication.builder(applicationId, ClientCredentialFactory.createFromSecret(applicationSecret))
.authority("https://login.microsoftonline.com/<<tenantId>>/")
.build();
} catch (MalformedURLException e) {
return null;
}
Set<String> scopeSet = Set.of(scopes);
ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(
scopeSet)
.build();
CompletableFuture<IAuthenticationResult> future = cca.acquireToken(clientCredentialParam);
return future.join().accessToken();
}
But the access token received in such way allows to receive photos only for users of the organization, which has the same tenant id with the tenant id of the bot app registered in Azure Active Directory App Registration.
Is it possible to receive the photos of the users from other organizations where my bot is installed, using client credentials flow token?
In this case I receive:
{
"error": {
"code": "UnknownError",
"message": "{\"error\":{\"code\":\"NoPermissionsInAccessToken\",\"message\":\"The token contains no permissions, or permissions can not be understood.\",\"innerError\":{\"oAuthEventOperationId\":\"d90f2331-a22a-44d5-889e-c0c14ea9129e\",\"oAuthEventcV\":\"n+NecjM040KWn+2G+e5oFQ.1\",\"errorUrl\":\"https://aka.ms/autherrors#error-InvalidGrant\",\"requestId\":\"0b6bb579-94a7-47ac-8ff9-26ee893e5cb0\",\"date\":\"2021-01-28T00:57:52\"}}}",
"innerError": {
"date": "2021-01-28T00:57:52",
"request-id": "0b6bb579-94a7-47ac-8ff9-26ee893e5cb0",
"client-request-id": "0b6bb579-94a7-47ac-8ff9-26ee893e5cb0"
}
}
}

I'm certainly no expert on auth, but here are two suggestions to look in to, in case this can help:
"https://login.microsoftonline.com/<>/" -> use "organizations" instead of tenant id
check what scopes you're using (you don't list what you're trying, so it might be this)

Just try using "https://login.microsoftonline.com/common" endpoint and see if you can get this working. Please read more on that in detail here.
You can even send REST request to individual tenant's get the access token and access the information accordingly. Please be aware of having scopes while sending request.

Related

Is there a way to avoid using the redirected form in Spring OAuth2 Authorization server when trying to get Authorization code? [duplicate]

I'm trying to create a local Java-based client that interacts with the SurveyMonkey API.
SurveyMonkey requires a long-lived access token using OAuth 2.0, which I'm not very familiar with.
I've been googling this for hours, and I think the answer is no, but I just want to be sure:
Is it possible for me to write a simple Java client that interacts with the SurveyMonkey, without setting up my own redirect server in some cloud?
I feel like having my own online service is mandatory to be able to receive the bearer tokens generated by OAuth 2.0. Is it possible that I can't have SurveyMonkey send bearer tokens directly to my client?
And if I were to set up my own custom Servlet somewhere, and use it as a redirect_uri, then the correct flow would be as follows:
Java-client request bearer token from SurveyMonkey, with
redirect_uri being my own custom servlet URL.
SurveyMonkey sends token to my custom servlet URL.
Java-client polls custom servlet URL until a token is available?
Is this correct?
Yes, it is possible to use OAuth2 without a callback URL.
The RFC6749 introduces several flows. The Implicit and Authorization Code grant types require a redirect URI. However the Resource Owner Password Credentials grant type does not.
Since RFC6749, other specifications have been issued that do not require any redirect URI:
RFC7522: Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants
RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
RFC8628: OAuth 2.0 Device Authorization Grant
In any case, if the grant types above do not fit on your needs, nothing prevent you from creating a custom grant type.
Not exactly, the whole point of the OAuth flow is that the user (the client you're accessing the data on behalf of) needs to give you permission to access their data.
See the authentication instructions. You need to send the user to the OAuth authorize page:
https://api.surveymonkey.net/oauth/authorize?api_key<your_key>&client_id=<your_client_id>&response_type=code&redirect_uri=<your_redirect_uri>
This will show a page to the user telling them which parts of their account you are requesting access to (ex. see their surveys, see their responses, etc). Once the user approves that by clicking "Authorize" on that page, SurveyMonkey will automatically go to whatever you set as your redirect URI (make sure the one from the url above matches with what you set in the settings for your app) with the code.
So if your redirect URL was https://example.com/surveymonkey/oauth, SurveyMonkey will redirect the user to that URL with a code:
https://example.com/surveymonkey/oauth?code=<auth_code>
You need to take that code and then exchange it for an access token by doing a POST request to https://api.surveymonkey.net/oauth/token?api_key=<your_api_key> with the following post params:
client_secret=<your_secret>
code=<auth_code_you_just_got>
redirect_uri=<same_redirect_uri_as_before>
grant_type=authorization_code
This will return an access token, you can then use that access token to access data on the user's account. You don't give the access token to the user it's for you to use to access the user's account. No need for polling or anything.
If you're just accessing your own account, you can use the access token provided in the settings page of your app. Otherwise there's no way to get an access token for a user without setting up your own redirect server (unless all the users are in the same group as you, i.e. multiple users under the same account; but I won't get into that). SurveyMonkey needs a place to send you the code once the user authorizes, you can't just request one.
You do need to implement something that will act as the redirect_uri, which does not necessarily need to be hosted somewhere else than your client (as you say, in some cloud).
I am not very familiar with Java and Servelets, but if I assume correctly, it would be something that could handle http://localhost:some_port. In that case, the flow that you describe is correct.
I implemented the same flow successfully in C#. Here is the class that implements that flow. I hope it helps.
class OAuth2Negotiator
{
private HttpListener _listener = null;
private string _accessToken = null;
private string _errorResult = null;
private string _apiKey = null;
private string _clientSecret = null;
private string _redirectUri = null;
public OAuth2Negotiator(string apiKey, string address, string clientSecret)
{
_apiKey = apiKey;
_redirectUri = address.TrimEnd('/');
_clientSecret = clientSecret;
_listener = new HttpListener();
_listener.Prefixes.Add(address + "/");
_listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
}
public string GetToken()
{
var url = string.Format(#"https://api.surveymonkey.net/oauth/authorize?redirect_uri={0}&client_id=sm_sunsoftdemo&response_type=code&api_key=svtx8maxmjmqavpavdd5sg5p",
HttpUtility.UrlEncode(#"http://localhost:60403"));
System.Diagnostics.Process.Start(url);
_listener.Start();
AsyncContext.Run(() => ListenLoop(_listener));
_listener.Stop();
if (!string.IsNullOrEmpty(_errorResult))
throw new Exception(_errorResult);
return _accessToken;
}
private async void ListenLoop(HttpListener listener)
{
while (true)
{
var context = await listener.GetContextAsync();
var query = context.Request.QueryString;
if (context.Request.Url.ToString().EndsWith("favicon.ico"))
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.Close();
}
else if (query != null && query.Count > 0)
{
if (!string.IsNullOrEmpty(query["code"]))
{
_accessToken = await SendCodeAsync(query["code"]);
break;
}
else if (!string.IsNullOrEmpty(query["error"]))
{
_errorResult = string.Format("{0}: {1}", query["error"], query["error_description"]);
break;
}
}
}
}
private async Task<string> SendCodeAsync(string code)
{
var GrantType = "authorization_code";
//client_secret, code, redirect_uri and grant_type. The grant type must be set to “authorization_code”
var client = new HttpClient();
client.BaseAddress = new Uri("https://api.surveymonkey.net");
var request = new HttpRequestMessage(HttpMethod.Post, string.Format("/oauth/token?api_key={0}", _apiKey));
var formData = new List<KeyValuePair<string, string>>();
formData.Add(new KeyValuePair<string, string>("client_secret", _clientSecret));
formData.Add(new KeyValuePair<string, string>("code", code));
formData.Add(new KeyValuePair<string, string>("redirect_uri", _redirectUri));
formData.Add(new KeyValuePair<string, string>("grant_type", GrantType));
formData.Add(new KeyValuePair<string, string>("client_id", "sm_sunsoftdemo"));
request.Content = new FormUrlEncodedContent(formData);
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
_errorResult = string.Format("Status {0}: {1}", response.StatusCode.ToString(), response.ReasonPhrase.ToString());
return null;
}
var data = await response.Content.ReadAsStringAsync();
if (data == null)
return null;
Dictionary<string, string> tokenInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(data);
return(tokenInfo["access_token"]);
}
}

MS Teams permissions for creating a group

Can someone help me understand how to add a permission to a MS-Graph API call?
I'm trying to create a new team/group but I'm getting a permission error. Obviously I need add the Team.Create permission if I want to create a Team. Which I did as seen in the image below
Here's the sample code where I tried to add the permission to the MSAL client request:
// Initialize Graph client
const client = graph.Client.init({
// Implement an auth provider that gets a token
// from the app's MSAL instance
authProvider: async (done) => {
try {
// Get the user's account
const account = await msalClient
.getTokenCache()
.getAccountByHomeId(userId);
let scope = process.env.OAUTH_SCOPES.split(',');
scope.push("Team.Create");
console.log("Added a extra permission request");
console.log("scope = " + scope);
if (account) {
// Attempt to get the token silently
// This method uses the token cache and
// refreshes expired tokens as needed
const response = await msalClient.acquireTokenSilent({
scopes: scope,
redirectUri: process.env.OAUTH_REDIRECT_URI,
account: account
});
console.log("\nResponse scope = " + JSON.stringify(response) + "\n");
// First param to callback is the error,
// Set to null in success case
done(null, response.accessToken);
}
} catch (err) {
console.log(JSON.stringify(err, Object.getOwnPropertyNames(err)));
done(err, null);
}
}
});
return client;
Then I get the following error:
The user or administrator has not consented to use the application with ID 'xxxxxxx'
named 'Node.js Graph Tutorial'. Send an interactive authorization request for this user and resource
I did give permissions to Team.Create in the Azure Active Directory, so how do I consent to this app gaining access? Note this code is the tutorial for learning Graph: https://learn.microsoft.com/en-us/graph/tutorials/node
Judging by the screenshot, you can't give admin consent to the permission as it is grayed out.
You'll need to try if you can grant user consent.
acquireTokenSilent won't work in this case since consent is needed.
You need to use one of the interactive authentication methods to trigger user authentication, at which time you can consent to the permission on your user's behalf.
In that sample specifically, you probably need to modify the scopes here: https://github.com/microsoftgraph/msgraph-training-nodeexpressapp/blob/08cc363e577b41dde4f6a72ad465439af20f4c3a/demo/graph-tutorial/routes/auth.js#L11.
And then trigger the /signin route in your browser.

How can I access group members with a service account?

I am attempting to use a service account to access members of a group. I have verified that I can do this using a normal OAuth2 token on behalf of a user, with a call to https://www.googleapis.com/admin/directory/v1/groups/{group}/members and the scope https://www.googleapis.com/auth/admin.directory.group.readonly.
I’d like to do the same with a service account, and I have added the service account email address as a group member and verified that View Members permissions are set to “All members of the group, All organization members”.
When I ask for a list of members, I receive this error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "Not Authorized to access this resource/api"
}
],
"code": 403,
"message": "Not Authorized to access this resource/api"
}
}
What do I need to do to authorize this service account to see the group?
Assume that you have the following
path to your serivce-account-key.josn
Domain wide delegation enabled on the service eaccount
admin email id Because of domain wide delegation, service account can use the admin email id.
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/admin.directory.group"]
credentials = service_account.Credentials.from_service_account_file(
PATH-TO-YOUR-SERVICE-ACCOUNT-FILE,
scopes=SCOPES, subject=ADMIN-EMAIL-ID)
service = build('admin', 'directory_v1', credentials=credentials)
group = "YOUR-GROUP-EMAIL-ID"
direct_members = service.members().list(groupKey=group).execute()["members"]
print(direct_members)
# Note that the above code would give only direct members.
# To get the direct members, set the `inclueDerivedMembership`
# argument to True as below.
all_members = service.members().list(
groupKey=group, inclueDerivedMembership=True).execute()["members"]
print(all_members)
The source of truth of this answer is here.
You can follow the steps outlined in the following API docs page to create the service account and perform a domain wide delegation of authority, please bear in mind you need the email address of any user who is a member of the group (userEmail in the code snippet below) so the service account can act on their behalf:
https://developers.google.com/admin-sdk/directory/v1/guides/delegation
The page includes a Java and Python examples of how to instantiate a com.google.api.services.admin.directory.Directory object using the service account and private key created on the Google Developers Console
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
.setServiceAccountScopes(DirectoryScopes.ADMIN_DIRECTORY_USERS)
.setServiceAccountUser(userEmail)
.setServiceAccountPrivateKeyFromP12File(
new java.io.File(SERVICE_ACCOUNT_PKCS12_FILE_PATH))
.build();

Unauthenticated call to Endpoint working for an API method with authentication

I am facing issue with Endpoints authentication.
This is the api code I am using
#ApiMethod(name = "myapiname.myapimethod", scopes = { Constants.EMAIL_SCOPE }, clientIds = {
Constants.WEB_CLIENT_ID, Constants.ANDROID_CLIENT_ID,
Constants.API_EXPLORER_CLIENT_ID }, audiences = { Constants.ANDROID_AUDIENCE })
public Result myapimethod(User user) throws OAuthRequestException, IOException {
// some work
return new Result();
}
In API explorer, it shows that the method requires Authorization, but it is getting successfully executed even without authorizing the request in API explorer.
Any help will be highly appreciated.
Thanks in advance.
Adding an User parameter alone won't check if the endpoint request is authenticated, we need to check that ourselves refer the documentation https://developers.google.com/appengine/docs/java/endpoints/auth#Java_Adding_a_user_parameter_to_methods_for_auth
`If an incoming client request has no authorization token or an invalid one, user is null. In your code, you need to check whether user is null and do ONE of the following, depending on the condition:
If the user is present, perform the authorized action.
If the user is null, throw an OAuthRequestException.
Alternatively, if the user is null, perform some action for an unauthorized client access if some sort of unauthorized access is desired.`

How do I use the Google API Explorer to test my own App Engine Endpoints using OAuth?

I have an Endpoints API deployed on App Engine. I have no problem using the Google API Explorer to make requests to API methods that do NOT require being logged in. The URL I'm using for that is:
https://developers.google.com/apis-explorer/?base=https://[MY_APP_ID].appspot.com/_ah/api
Where I am stuck is calling API methods that require the user to be logged in, such as this one:
#ApiMethod(name = "config.get",
clientIds = {"[MY_CLIENT_ID].apps.googleusercontent.com", "com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID"},
audiences = {"[MY_APP_ID].appspot.com"},
scopes = {"https://www.googleapis.com/auth/userinfo.email"})
public Config getConfig(User user) throws OAuthRequestException {
log.fine("user: " + user);
if (user == null) {
throw new OAuthRequestException("You must be logged in in order to get config.");
}
if (!userService.isUserAdmin()) {
throw new OAuthRequestException("You must be an App Engine admin in order to get config.");
}
...
On the API Explorer there's a switch top right that, when clicked, allows me to specify scopes and authorise. I'm doing that with just the userinfo.email scope checked. It makes no difference. The response I get from my call is:
503 Service Unavailable
- Show headers -
{
"error": {
"errors": [
{
"domain": "global",
"reason": "backendError",
"message": "java.lang.IllegalStateException: The current user is not logged in."
}
],
"code": 503,
"message": "java.lang.IllegalStateException: The current user is not logged in."
}
}
Back when Endpoints was in Trusted Tester phase, I remember there being a manual step in the OAuth2 Playground to get an ID token instead of an access token or some such thing. If that is still required, any mention of that seems to have disappeared from the Endpoints docs now and I see now way to swap out tokens in the API Explorer either.
I see you've got "com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID" in quotes. If that's not a typo in your transcription to Stack Overflow, that's a problem. The value is already a string, so you're just passing in the text com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID (not the actual client ID) as the whitelisted scope. That won't work. Try this instead:
#ApiMethod(name = "config.get",
clientIds = {"[MY_CLIENT_ID].apps.googleusercontent.com", com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID},
audiences = {"[MY_APP_ID].appspot.com"},
scopes = {"https://www.googleapis.com/auth/userinfo.email"})
Edit: isUserAdmin is unsupported within Endpoints, and is likely a secondary cause of error. I'd suggest filing a feature request for supporting this method on the provided User object (we likely won't provide support for the user service itself, so it's separate from OAuth login.)
I don't know when this was introduced, but if you use OAuth2, instead of UserService.isUserAdmin() you can use OAuthServiceFactory.getOAuthService().isUserAdmin(EMAIL_SCOPE) where EMAIL_SCOPE is "https://www.googleapis.com/auth/userinfo.email".
This makes it easy to use the old OpenId or OAUth2:
boolean isAdmin = false;
try {
isAdmin = userService.isUserAdmin());
} catch (IllegalStateException e1) {
try {
isAdmin = OAuthServiceFactory.getOAuthService().isUserAdmin(EMAIL_SCOPE);
} catch (Exception e2) {}
}
The original question was asked several years ago, but maybe this will help others.

Resources