I've adapted the Java console application letting users sign-in with username/password to call Microsoft Graph API on behalf of them, but instead of retrieving basic user data to send emails.
However, while the original example works fine (I am getting user email, see commented code below), I am getting this error when sending emails:
Graph service exception Error code: NoPermissionsInAccessToken
Error message: The token contains no permissions, or permissions can not be understood.
For this operation Mail.Send scope needs to be defined in Azure portal and it should be sufficient to use without Admin consent.
I use these Microsoft dependencies:
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.graph</groupId>
<artifactId>microsoft-graph</artifactId>
<version>1.6.0</version>
</dependency>
This is the actual code:
public class MicrosoftGraphMailer {
private final static String CLIENT_APP_ID = "{real-client-app-id}";
private final static String AUTHORITY = "https://login.microsoftonline.com/{real-tenant-id}/";
public static void main(String[] args) throws Exception {
String user = "{real-user}";
String password = "{real-password}";
String token = getUserPasswordAccessToken(user, password).accessToken();
System.out.println(token);
SimpleAuthProvider authProvider = new SimpleAuthProvider(token);
IGraphServiceClient graphClient = GraphServiceClient.builder().authenticationProvider(authProvider).buildClient();
Message message = new Message();
message.subject = "My subject";
ItemBody body = new ItemBody();
body.contentType = BodyType.HTML;
body.content = "<h1>My HTML body</h1>";
message.body = body;
List<Recipient> toRecipientList = new LinkedList<>();
Recipient toRecipient = new Recipient();
EmailAddress emailAddress = new EmailAddress();
emailAddress.address = "{real-recipient}";
toRecipient.emailAddress = emailAddress;
toRecipientList.add(toRecipient);
message.toRecipients = toRecipientList;
graphClient.me()
.sendMail(message, false)
.buildRequest()
.post();
/*
String mail = graphClient.me()
.buildRequest()
.get().mail;
System.out.println(mail);
*/
}
private static IAuthenticationResult getUserPasswordAccessToken(String user, String password) throws Exception {
PublicClientApplication app = PublicClientApplication.builder(CLIENT_APP_ID).authority(AUTHORITY).build();
Set<String> scopes = new HashSet<>(Arrays.asList("Mail.Send"));
UserNamePasswordParameters userNamePasswordParam = UserNamePasswordParameters.builder(
scopes, user, password.toCharArray())
.build();
return app.acquireToken(userNamePasswordParam).get();
}
private static class SimpleAuthProvider implements IAuthenticationProvider {
private String accessToken = null;
public SimpleAuthProvider(String accessToken) {
this.accessToken = accessToken;
}
#Override
public void authenticateRequest(IHttpRequest request) {
request.addHeader("Authorization", "Bearer " + accessToken);
}
}
}
Basically I need console daemon app for sending emails on behalf of me without any user interaction. Credentials will be stored outside the app. I don't need permissions to send emails on behalf of arbitrary user.
usually that error message only occurs if its not sending a token to graph or a valid token to graph. To debug, I would first get the token and decode it by either pasting it in jwt.ms or something to see if the required scopes are in the token as expected.
I also wonder if not requesting the user.read scope causes issues for mail.send
Also, you said admin consent need not be given, but in your case it does. because if the admin doesn't consent, then the user must consent. but since you are doing this headless, there is no opportunity for the user to consent. meaning no one consented for this application to send mail as you.
Related
I am trying to use microsoft graph api and I need authorization code for using that.
Redirecting the application to microsoft login site is not possible in my application.
I need to call this and for that I require authProvider:
IGraphServiceClient graphClient = GraphServiceClient.builder().authenticationProvider(authProvider)
.buildClient();
I am using this for creating authProvider:
UsernamePasswordProvider authProvider = new UsernamePasswordProvider(CLIENT_ID,
Arrays.asList("https://graph.microsoft.com/user.read", "https://graph.microsoft.com/Mail.ReadWrite",
"https://graph.microsoft.com/Calendars.ReadWrite"),
USERNAME, PASSWORD, NationalCloud.Global,
TENANT, CLIENT_SECRET)
On using this I get error:
OAuthProblemException{error='invalid_grant', description='AADSTS65001: The user or administrator has not consented to use the application with ID 'e2bfebf6-cc77-49ec-82a3-28756ad377e5' named 'Milpitas Communications'. Send an interactive authorization request for this user and resource.
Trace ID: a2b91757-4849-4680-a089-001831ef7b00
Correlation ID: ae894060-a2ce-444c-9889-96fd3cdfaea7
I also tried to use this to create authprovider:
AuthorizationCodeProvider authProvider = new AuthorizationCodeProvider(CLIENT_ID,
Arrays.asList("https://graph.microsoft.com/user.read", "https://graph.microsoft.com/Mail.ReadWrite",
"https://graph.microsoft.com/Calendars.ReadWrite"),
AUTHORIZATION_CODE, REDIRECT_URL, NationalCloud.Global, "common",
CLIENT_SECRET);
To run the above I need authorization code, can anyone please suggest How I can get the code internally in spring boot application, as my application cannot have client input(for auth) on this?
Or alternatively Is there some other way I can use the IGraphServiceClient for creating a calendar event?
Replace Arrays.asList("https://graph.microsoft.com/user.read", "https://graph.microsoft.com/Mail.ReadWrite", "https://graph.microsoft.com/Calendars.ReadWrite") with Arrays.asList("https://graph.microsoft.com/.default") can resolve this issue.
My code for your reference:
public static void main(String[] args) {
String USERNAME = "{username}";
String PASSWORD = "{password}";
String TENANT = "{tenantID}";
String CLIENT_ID = "{clientID}";
String CLIENT_SECRET = "{clientSecret}";
UsernamePasswordProvider authProvider = new UsernamePasswordProvider(CLIENT_ID,
Arrays.asList("https://graph.microsoft.com/.default"),
USERNAME, PASSWORD, NationalCloud.Global,
TENANT, CLIENT_SECRET);
IGraphServiceClient graphClient = GraphServiceClient
.builder()
.authenticationProvider(authProvider)
.buildClient();
User user = graphClient.me().buildRequest().get();
System.out.println(user.userPrincipalName);
}
I have a web app where users have to authenticate using Google sign-in. I do this because I need to grab their email address. When they fill out the fields on the page, all that data is stored in a google sheet alongside their email address (for auditing purposes incase something is askew with the data). Unfortunately what's happening is that if user A signs in, and does some work and at the same time user B logs in, when user A submits data, they will be submitting user B's email address (as does user B). In short, the latest person to log in, that email address is used. There is no database and I'm not storing any cookies. When they refresh the page, they have to re-authenticate. I am using Angular 7 and Java. Here is the code that I used:
ngOnInit() {
gapi.load('auth2', () => {
this.auth2 = gapi.auth2.init({
client_id: 'CLIENT_ID_HERE',
// Scopes to request in addition to 'profile' and 'email'
scope: 'https://www.googleapis.com/auth/spreadsheets'
});
});
}
signInWithGoogle(): void {
this.auth2.grantOfflineAccess().then((authResult) => {
this.authCode = authResult['code'];
this.fetchData();
});
}
authCode is bound to the child component so it can be passed as query param to the java code for google auth.
this.seriesService.submitSeriesData(matchList, this.authToken).subscribe(res => {.....);
The google auth java code is so:
private static final String APPLICATION_NAME = "Google Sheets API Java";
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
private static final List<String> SCOPES = Collections.singletonList(SheetsScopes.SPREADSHEETS);
private static final String CLIENT_SECRET_DIR = "/client_secret.json";
private static GoogleTokenResponse tokenResponse = null;
public static String getEmailAddress() throws IOException {
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
String email = payload.getEmail();
return email;
}
public static Sheets getSheetsService1(String token, String redirectUri) throws IOException, GeneralSecurityException {
// Exchange auth code for access token
InputStream in = GoogleAuthUtil.class.getResourceAsStream(CLIENT_SECRET_DIR);
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));
tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
"https://www.googleapis.com/oauth2/v4/token",
clientSecrets.getDetails().getClientId(),
clientSecrets.getDetails().getClientSecret(),
token,
redirectUri)
.execute();
String accessToken = tokenResponse.getAccessToken();
GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Sheets service = new Sheets.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential)
.setApplicationName("MY APP HERE")
.build();
return service;
}
And the endpoint:
#RequestMapping(value="series/data", method = RequestMethod.POST, consumes="application/json")
public boolean submitSeriesMatchData(#RequestBody(required=true) SubmitStatsDto request) throws IOException, GeneralSecurityException, Exception {
if (service == null) {
service = GoogleAuthUtil.getSheetsService1(request.getToken(), this.redirectUri);
}
......
}
1) User clicks on the google sign in button
2) They select email and auth with google
3) I receive an auth code back and store it in ng.
4) Every REST call is passed said token to auth with google, and every endpoint calls getSheetsService1 which authenticates w/ token. (multiple endpoints, I only showed one above)
5) I get email from that tokenResponse.
Any ideas? This site will not have a database/users/local logins. Thank you.
When I get a token by authorization code (authContext.acquireTokenByAuthorizationCode), I get a JWT (idToken) that is signed and has the proper headers:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "wLLmYfsqdQuWtV_-hnVtDJJZM3Q",
"kid": "wLLmYfsqdQuWtV_-hnVtDJJZM3Q"
}
but when I use the refresh token to get a new token (authContext.acquireTokenByRefreshToken(...)), it returns an unsigned JWT:
{
"typ": "JWT",
"alg": "none"
}
How do I get it to give me a signed JWT?
return authContext.acquireTokenByRefreshToken(
refreshToken,
new ClientCredentials(
clientId,
clientSecret
),
null
);
I did not reproduce your issue on my side. I followed this tutorial to get Authentication code and acquire access token and refresh token with below code successfully. Please refer to it.
import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.aad.adal4j.ClientCredential;
import java.net.URI;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class GetTokenByAuthenticationCode {
private static final String APP_ID = "***";
private static final String APP_SECRET = "***";
private static final String REDIRECT_URI = "http://localhost:8080";
private static final String tenant = "***";
public static void main(String[] args) throws Exception {
String authority = "https://login.microsoftonline.com/" + tenant + "/oauth2/authorize";
ExecutorService service = Executors.newFixedThreadPool(1);
String code = "***";
AuthenticationContext context = new AuthenticationContext(authority, true, service);
URI url = new URI(REDIRECT_URI);
Future<AuthenticationResult> result = context.acquireTokenByAuthorizationCode(
code,
url,
new ClientCredential(APP_ID, APP_SECRET),
null
);
String token = result.get().getAccessToken();
System.out.println(token);
String refreshToken = result.get().getRefreshToken();
System.out.println(refreshToken);
Future<AuthenticationResult> result1 = context.acquireTokenByRefreshToken(
refreshToken,
new ClientCredential(APP_ID, APP_SECRET),
null
);
String tokenNew = result1.get().getAccessToken();
String refreshTokenNew = result1.get().getRefreshToken();
System.out.println(tokenNew);
System.out.println(refreshTokenNew);
}
}
Decode:
Update Answer:
Firstly, sorry for the mistake. I replaced getIdToken with getAccessToken, the result is as same as you.Then I searched the response parameters in Authorize access to Azure Active Directory web applications using the OAuth 2.0 code grant flow, you could find the statement of id_token parameter.
An unsigned JSON Web Token (JWT) representing an ID token. The app can
base64Url decode the segments of this token to request information
about the user who signed in. The app can cache the values and display
them, but it should not rely on them for any authorization or security
boundaries.
So, the id token just a segment which can't be relied on. If you want to get the complete id token, please refer to the openId flow.
This code is only for a user.
I'm looking for the way to make this for multiple user.
Please, give me some tips.
To run the batch job, I know that some variables (is_authorized, requestToken and accessToken) should be removed. I tried to use spring-social-tumblr(on github)but it was not easy to use ConnectionRepository. so I tried to use signpost.
After signing with signpost, how could I set the user access token for multi-user?
Is it right to use OAuthConsumer class?
#Controller
public class TumblrProfileController {
private OAuthService service;
private Token requestToken; //should be removed for multiuser
private Token accessToken; // same above
private static final String PROTECTED_RESOURCE_URL = "http://api.tumblr.com/v2/user/info";
#Autowired
private JobLauncher jobLauncher;
#Autowired
private Job job;
#Inject
private ConnectionRepository connectionRepository;
Logger log = LoggerFactory.getLogger(this.getClass());
private boolean is_authorized = false;
#RequestMapping(value = "/tumblr/webrequest", method = RequestMethod.GET)
public String home(OAuthConsumer user, Model model) {
final String PROTECTED_RESOURCE_URL = "http://api.tumblr.com/v2/user/info";
service = new ServiceBuilder().provider(TumblrApi.class).apiKey("clientKey") .apiSecret("secretKey").callback("http://localhost:8080/pen/tumblr/login").build();
log.info("Fetching the Request Token...");
// Obtain the Request Token
requestToken = service.getRequestToken();
log.info("Now go and authorize Scribe here:");
String redirectUrl = service.getAuthorizationUrl(requestToken);
log.info(redirectUrl);
return "redirect:" + redirectUrl;
}
#RequestMapping(value = "/tumblr/login", method = RequestMethod.GET)
public String login(#RequestParam(required = false) final String oauth_verifier) {
Verifier verifier = new Verifier(oauth_verifier);
// Trade the Request Token and Verfier for the Access Token
log.info("Trading the Request Token for an Access Token...");
accessToken = service.getAccessToken(requestToken, verifier);
log.info("Got the Access Token!");
log.info("(if your curious it looks like this: " + accessToken + " )");
// Now let's go and ask for a protected resource!
log.info("Now we're going to access a protected resource...");
OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
service.signRequest(accessToken, request);
Response response = request.send();
log.info("Got it! Lets see what we found...");
log.info(response.getBody());
log.info("Thats it man! Go and build something awesome with Scribe! :)");
run();
is_authorized = true;
return "tumblr/feed";
}
public void run() {
try {
if(! is_authorized ) return;
OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
service.signRequest(accessToken, request);
Response response = request.send();
log.info("[2nd Call ]Got it! Lets see what we found...");
log.info(response.getBody());
} catch (Exception e) {
e.printStackTrace();
}
}
Although I've not used Spring Social Tumblr (it's a community-led project), the process shouldn't be much (or any) different than using Spring Social with Facebook or Twitter.
Note that in this code, you're doing too much work. You're going to the trouble of redirecting to Tumblr for authorization and then handling the redirect to exchange the request token and verifier for an access token. Certainly, those things must be done, but with Spring Social there's absolutely no reason why you have to do those things. That's what ConnectController is for. ConnectController handles all of that, creates and persists the connection, and (generally speaking) you never have to muck about with OAuth directly. And, it has no problem working with multiple users.
May I recommend that you look at the Spring Social Showcase example at https://github.com/spring-projects/spring-social-samples/tree/master/spring-social-showcase to see how it's done? That example connects with Facebook, Twitter, and LinkedIn, but there's really no reason why it couldn't connect to Tumblr in the same fashion. For a much simpler approach that leverages Spring Boot and automatic configuration, you might also have a look at https://github.com/spring-projects/spring-social-samples/tree/master/spring-social-showcase-boot. (Note, however, that Spring Boot doesn't have autoconfig for Tumblr, so there'd still be some manual config required.)
I'm trying to write a simple Jenkins plug-in with integration with Box, but I always get this error:
=== Box's OAuth Workflow ===
Fetching the Authorization URL...
Got the Authorization URL!
Now go and authorize Scribe here:
https://www.box.com/api/oauth2/authorize?client_id=abc123&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fjenkins%2Fconfigure&response_type=code&state=authenticated
And paste the authorization code here
>>xyz9876543
Trading the Request Token for an Access Token...
Exception in thread "main" org.scribe.exceptions.OAuthException: Cannot extract an acces token. Response was: {"error":"invalid_request","error_description":"Invalid grant_type parameter or parameter missing"}
at org.scribe.extractors.JsonTokenExtractor.extract(JsonTokenExtractor.java:23)
at org.scribe.oauth.OAuth20ServiceImpl.getAccessToken(OAuth20ServiceImpl.java:37)
at com.example.box.oauth2.Box2.main(Box2.java:40)
Box2 class (for testing) :
public class Box2 {
private static final Token EMPTY_TOKEN = null;
public static void main(String[] args) {
// Replace these with your own api key and secret
String apiKey = "abc123";
String apiSecret = "xyz987";
OAuthService service = new ServiceBuilder().provider(BoxApi.class)
.apiKey(apiKey).apiSecret(apiSecret)
.callback("http://localhost:8080/jenkins/configure")
.build();
Scanner in = new Scanner(System.in);
System.out.println("=== Box's OAuth Workflow ===");
System.out.println();
// Obtain the Authorization URL
System.out.println("Fetching the Authorization URL...");
String authorizationUrl = service.getAuthorizationUrl(EMPTY_TOKEN);
System.out.println("Got the Authorization URL!");
System.out.println("Now go and authorize Scribe here:");
System.out.println(authorizationUrl);
System.out.println("And paste the authorization code here");
System.out.print(">>");
Verifier verifier = new Verifier(in.nextLine());
System.out.println();
// Trade the Request Token and Verfier for the Access Token
System.out.println("Trading the Request Token for an Access Token...");
Token accessToken = service.getAccessToken(EMPTY_TOKEN, verifier);
System.out.println("Got the Access Token!");
System.out.println("(if your curious it looks like this: "
+ accessToken + " )");
System.out.println();
}
}
BoxApi class:
public class BoxApi extends DefaultApi20 {
private static final String AUTHORIZATION_URL =
"https://www.box.com/api/oauth2/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=authenticated";
#Override
public String getAccessTokenEndpoint() {
return "https://www.box.com/api/oauth2/token?grant_type=authorization_code";
}
#Override
public String getAuthorizationUrl(OAuthConfig config) {
return String.format(AUTHORIZATION_URL, config.getApiKey(),
OAuthEncoder.encode(config.getCallback()));
}
#Override
public Verb getAccessTokenVerb(){
return Verb.POST;
}
#Override
public AccessTokenExtractor getAccessTokenExtractor() {
return new JsonTokenExtractor();
}
}
I'm not sure how I get these exceptions. Can anyone who knows the Box API tell me if I've done anything wrong with it?
The request for the access token needs to be in the form of a POST request, with the parameters included in the body of the request–it looks like you're sending a GET with the parameters as URL parameters.