My Authorization Client: Angular, Resource Server: Java Spring Boot, Authorization Server: Azure Active Directory
I am using oAuth2 to login via Angular via the PKCE Authorization Flow and then pass the token to the back end.
I am able to access the token in my back end via the Authorization Beaer Header, but when I go to use that token to access Microsoft Graph API, I am getting an Invalid token exception.
com.microsoft.graph.http.GraphServiceException: Error code: InvalidAuthenticationToken
Error message: CompactToken parsing failed with error code: 80049217
I am not sure why it is causing this error, because its valid and I can verify via https://jwt.io/
and access my other protected api in postman with the token.
AuthProvider.java
public class AuthProvider implements IAuthenticationProvider {
private String accessToken = null;
public AuthProvider(String accessToken) {
this.accessToken = accessToken;
}
#Override
public void authenticateRequest(IHttpRequest request) {
// Add the access token in the Authorization header
request.addHeader("Authorization", "Bearer " + accessToken);
}
}
SecurityConfiguration.java
http.cors().and()
.authorizeRequests().antMatchers("/home").permitAll()
.and()
.authorizeRequests().antMatchers("/actuator/health").permitAll()
.and()
.authorizeRequests().antMatchers("/**").authenticated()
.and()
.oauth2ResourceServer().jwt();
GraphAPIController.java
private static IGraphServiceClient graphClient = null;
private static AuthProvider authProvider = null;
private static void ensureGraphClient(String accessToken) {
if (graphClient == null) {
// Create the auth provider
authProvider = new AuthProvider(accessToken);
// Create default logger to only log errors
DefaultLogger logger = new DefaultLogger();
logger.setLoggingLevel(LoggerLevel.ERROR);
// Build a Graph client
graphClient = GraphServiceClient
.builder()
.authenticationProvider(authProvider)
.logger(logger)
.buildClient();
}
}
#GetMapping("/getUser")
public static User getUser(#RequestHeader(value="Authorization") String token) {
System.out.println("THE TOKEN: " +token);
ensureGraphClient(token);
// GET /me to get authenticated user
User me = graphClient
.me()
.buildRequest()
.get();
System.out.println("THE USER: " + me);
return me;
}
My Angular Setup:
app.module:
import { OAuthModule } from 'angular-oauth2-oidc';
app.component.ts
Postman:
An access token can only be for one resource. I can see that you configure scope: 'openid api://{appid}/app' in your Angular Setup. It means the access token is for this resource api://{appid}/app rather than Microsoft Graph https://graph.microsoft.com. That is why you got the InvalidAuthenticationToken Error.
So if you want to call Microsoft Graph in your backend API, you need to consider OAuth 2.0 On-Behalf-Of flow. The OAuth 2.0 On-Behalf-Of flow (OBO) serves the use case where an application invokes a service/web API, which in turn needs to call another service/web API.
In your case, your backend API is web API A and Microsoft Graph is web API B.
A sample for your reference.
Related
I am using Azure AD to carry out the users authentication of a spring boot app.
I need to generate the token for some reasons from spring boot code and return it. So far this is what I have achieve.
1 Azure Ad config.
I have config a app within azure Ad and I have register One user and one group.
within the app i also have create a secret
Now in my spring boot app i have added the JWT filter and some config (I am not going to explain the full config because it will take a while)
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Profile("AzureAdSecurized")
public class AzureSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private RestAuthenticationExceptionHandler restAuthenticationExceptionHandler;
#Autowired
private AADAppRoleStatelessAuthenticationFilter aadAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS );
http.headers().frameOptions().disable();
http.addFilterAfter(aadAuthenticationFilter, UsernamePasswordAuthenticationFilter.class );
http.addFilterBefore( new CorsFilter(), ChannelProcessingFilter.class );
http.exceptionHandling().authenticationEntryPoint( restAuthenticationExceptionHandler );
//Configuracion Endpoints
http.authorizeRequests().antMatchers( "/auth/login**" ).permitAll()
.antMatchers( "/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**" ).permitAll()
.antMatchers( "/actuator/**" ).permitAll().anyRequest().authenticated();
}
I also have added the following properties:
azure:
activedirectory:
tenant-id: 7XXXXX
client-id: 5XXXXX
session-stateless: true
spring:
security:
oauth2:
client:
registration:
azure:
client-id: 5XXX
client-secret: dXXXX
if I go to the following URL of microsoft:
https://login.microsoftonline.com//oauth2/authorize?client_id=&response_type=id_token&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin&nonce=7362CAEA-9CA5–4B43–9BA3–34D7C303EBA7
I get a perfect Token in a redirection like: http://localhost:8080/login#id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllNRUxIVDBndmIwbXhvU0RvWWZvbWp
with this token I perfectly pass the aadAuthenticationFilter of spring boot security.
The point is that i have to generate this token from the app.
In my spring boot app I have algo add:
I have follow a tutorial but i dont no remeber url
#RestController
#RequestMapping(LoginPaths.AUTH)
#Profile("AzureAdSecurized")
public class AADLoginController {
private static final Logger LOG = LoggerFactory.getLogger( AADLoginController.class );
#RequestMapping(value = LoginPaths.LOGIN, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String generateTokenAzureAD() throws MalformedURLException, ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool( 1 );
AuthenticationContext context = new AuthenticationContext(
"https://login.microsoftonline.com/<My-tenant>/oauth2/v2.0/authorize", false,
service );
Future<AuthenticationResult> future = context
.acquireToken( "https://graph.microsoft.com",
"<app-id>", "<username>",
"<pass>", null );
AuthenticationResult result = future.get();
LOG.info( "Access Token - " + result.getAccessToken() );
LOG.info( "Refresh Token - " + result.getRefreshToken() );
LOG.info( "ID Token - " + result.getIdToken() );
return "Bearer " + result.getAccessToken();
}
}
by passing username and password with classes of library "com.microsoft.aad.adal4j" I am trying to generate I token.
The token I receive is the following:
As you can see the token has information related with my user and so on and it seems to be correct, but the signature is not valid and if a send a request to my app with such token. obviously it gives me and error for invalid signature.
What I am doing wrong?
Thank you very much
Your token is correct. It is just a specific token for graph api. You will see a nonce in Jwt.Header. This means you need special processing. Normal processing will fail.
Update:
Access tokens are opaque blobs of text that are for the resource only.
If you're a client getting a token for Graph, assume that it's an
encrypted string that you should never look at - sometimes it will be.
We use a special token format for Graph that they know how to validate
- you shouldn't be looking at access tokens if they're not for you.
Reference:
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609#issuecomment-529537264
I am usisng the dependency instead:
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
</dependency>
And generating tokens this way:
public TokenDTO generateTokenAzureAD(CredentialsDTO credentialsDTO) {
LoginValidator.validateLoginRequest( credentialsDTO );
PublicClientApplication app;
String AUTHORITY = authorityUrl + addTenantId;
try {
app = PublicClientApplication.builder( addAppId ).authority( AUTHORITY ).build();
} catch (MalformedURLException e) {
throw new MyException( ErrorCodes.ERROR_ADD_AUTHORITY_URL_NOT_VALID );
}
Set<String> scopes = Collections.singleton( addAppId + addAppScope );
UserNamePasswordParameters parameters = UserNamePasswordParameters
.builder( scopes, credentialsDTO.getUsername(), credentialsDTO.getPassword().toCharArray() ).build();
Future<IAuthenticationResult> result = app.acquireToken( parameters );
IAuthenticationResult auth;
try {
auth = result.get();
} catch (InterruptedException | ExecutionException e) {
throw new MyException( ErrorCodes.ERROR_ADD_AUTHORITY_URL_NOT_VALID );
}
return TokenUtils.fromAddAuthToTokenDTO( auth );
}
Where
authorityUrl = https://login.microsoftonline.com/
addTenantId = azureID tenant
addAppId = azureID app ID
addAppScope = /User.Read
for scopes yo can create a new one in "Expose API" option of azure AD app menu
Thanks also to #TonyJu
We have a Daemon application that uses the EWS API to access office365/Exchange server with basic authentication. I am trying to implement the Oauth2. There are a lot of documents. However, they are often out of date and caused more confusion. I followed this document https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-daemon-overview, which seems up-to-date. I did the following steps:
Register App
Document: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-daemon-app-registration
- Registered a secret with application password in Azure AD, i.e. certificate is used. The generated secret is recorded.
- selected the “Accounts in this organizational directory only”.
- Requested API Permission for Application permissions for Exchange full_access_as_app and Mail.Read. Admin consent is granted.
Get Token
Document: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-daemon-acquire-token?tabs=java
I prototyped to use Protocol to get token
POST /{tenant}/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id={myAppClientId}
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret={myAppSecret}
&grant_type=client_credentials
I got token with
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "……thetoken…"
}
Call EWS API in my App
My App works with the Basic Authentication. I modified it by adding the Authorization header ("Authorization", "Bearer " + accessToken); Basially the prepareWebRequest() function is overriden by adding the Authorization header. Compared with a Basic Authentication case, the request has the additional Authorization header with the Bearer token.
For the same EWS API call that the Basic Authorization had worked, the response is 401 with
x-ms-diagnostics
2000003;reason="The audience claim value is invalid for current resource. Audience claim is 'https://graph.microsoft.com', request url is 'https://outlook.office365.com/EWS/Exchange.asmx' and resource type is 'Exchange'.";error_category="invalid_resource"
Researched in stackoverflow, people suggested to use the following as scope value to get token in step 2:
https://outlook.office365.com/full_access_as_app
https://outlook.office.com/Mail.Read
I tried and both returned “invalid_scope” error. It seems both worked before but not anymore. Following the working scope value format, I tried to use https://outlook.office.com/.default as scope value. I was able to get a token! However, when I use this token in EWS API to access the mailbox, I got 500 error instead of the 401.
What are the right things to do to make it work? What is the right Scope to access an office365 mail box?
More Code Snippets
This is a new class added for oauth2
package microsoft.exchange.webservices.data;
import java.util.Map;
public final class BearerTokenCredentials extends ExchangeCredentials {
private static final String BEARER_TOKEN_FORMAT_REGEX = "^[-._~+/A-Za-z0-9]+=*$";
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER_AUTH_PREAMBLE = "Bearer ";
private String token;
public String getToken() {
return token;
}
public BearerTokenCredentials(String bearerToken) {
if (bearerToken == null) {
throw new IllegalArgumentException("Bearer token can not be null");
}
this.validateToken(bearerToken);
this.token = bearerToken;
}
protected void validateToken(String bearerToken) throws IllegalArgumentException {
if (!bearerToken.matches(BEARER_TOKEN_FORMAT_REGEX)) {
throw new IllegalArgumentException("Bearer token format is invalid.");
}
}
#Override
public void prepareWebRequest(HttpWebRequest request) {
Map<String, String> headersMap = request.getHeaders();
String bearerValue = BEARER_AUTH_PREAMBLE + token;
headersMap.put(AUTHORIZATION, bearerValue);
//headersMap.put("X-AnchorMailbox","esj_office365_imap#genesyslab.onmicrosoft.com");
request.setHeaders(headersMap);
}
}
Use the token to acceess EWS/Exchange ews-java-api 2.0-patched
ExchangeService service = new
ExchangeService(ExchangeVersion.Exchange2010_SP2); //version is
Exchange2010_SP2
service.setTraceEnabled(true);
BearerTokenCredentials credentials = new BearerTokenCredentials("thetoken");
service.setCredentials(credentials);
service.setUrl(new
URI(host));//https://outloook.office365.com/EWS/Exchange.asmx
try{
Folder.bind(service, WellKnownFolderName.Inbox);
}catch(Exception e)
{
//The remote server returned an error: (500)Internal Server Error
}
The code you use to connect to the Office365 Mailbox still needs to use EWS Impersonation eg
service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, mailboxName);
Where MailboxName is the Mailbox you want to connect to.
I'm developing an OAuth2.0 "CLIENT" application which call some APIs(secured by oauth2.0).
I'm using OAuth2.0RestTemplate which contains CLIENT_ID, CLIENT_SECRET, username and password. The code for calling OAuth2.0 secured APIs looks like this:
#Bean
OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
List<String> Scopes = new ArrayList<String>(2);
Scopes.add("read");
Scopes.add("write");
resource.setClientAuthenticationScheme(AuthenticationScheme.header);
resource.setId("*****");
resource.setAccessTokenUri(tokenUrl);
resource.setClientId("*****");
resource.setClientSecret("*****");
resource.setGrantType("password");
resource.setScope(Scopes);
resource.setUsername("*****");
resource.setPassword("*****");
return resource;
}
#Autowired
private OAuth2RestTemplate restTemplate;
Map<String, String> allCredentials = new HashMap<>();
allCredentials.put("username", "***");
allCredentials.put("password", "***");
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setAll(allCredentials);
ParameterizedTypeReference<List<MyObject>> responseType = new ParameterizedTypeReference<List<MyObject>>() { };
ResponseEntity<List<MyObject>> response = restTemplate.exchange("https://***.*****.com/api/*****/*****",
HttpMethod.GET,
null,
responseType);
AllCities all = new AllCities();
all.setAllCities(response.getBody());
As you can see everytime I want to call a service the code get a new ACCESS TOKEN which is wildly wrong!!! My question is how can I automatically receive and store the issued token in my application an use it until it expires and then automatically get a new one?
On the other hand my token only contains access token and doesn't contain refresh token(I don't know why!!! this is so weird!!!)
Hello you can design like google client library.
First step you need to create the datastore for store the token in your directory like C:/User/soyphea/.token/datastore.
Before you load your function retrieve access_token_store. Your access token should have expired_in.
if(access_token_store from your datastore !=null && !expired){
access_token = access_token_store.
} else {
access_token = Your RestTemplate function for retrieve access_token.
}
finally you can retrieve access_token.
In spring security oauth2 if you want to support refresh_token you need to set,
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("resource-serv")
.scopes("read")
.resourceIds("my-resource")
.secret("secret123")
.and()
.withClient("app")
.authorizedGrantTypes("client_credentials", "password", "refresh_token")
.scopes("read")
.resourceIds("my-resource")
.secret("appclientsecret");
}
First of all you have define that your app is a Oaut2App for this in Spring boot you can use the annotation #EnableOAuth2Client in your code and configure the client application metadata in your applicaition.yml. A skeleton client app can be like below:
#EnableOAuth2Client
#SpringBootApplication
public class HelloOauthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(HelloOauthServiceApplication.class, args);
}
#Bean
public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource){
return new OAuth2RestTemplate(resource);
}
}
application.yml
security:
oauth2:
client:
clientId: client
clientSecret: secret
accessTokenUri: http://localhost:9090/oauth/token
userAuthorizationUri: http://localhost:9090/oauth/authorize
auto-approve-scopes: '.*'
registered-redirect-uri: http://localhost:9090/login
clientAuthenticationScheme: form
grant-type: passwordR
resource:
token-info-uri: http://localhost:9090/oauth/check_token
in this way you have guarantee that the OAuth2RestTemplate of spring will use and upgrade the token
I was following this tutorial: https://developers.docusign.com/esign-rest-api/code-examples/config-and-auth
(I used java)
In the third step I dont know how to get the code that is sent back form DocuSign as query param in the redirect uri:
// Java request auth token
**String code = "{ENTER_AUTH_CODE_FROM_PREVIOUS_STEP}";**
// assign it to the token endpoint
apiClient.getTokenEndPoint().setCode(code);
// optionally register to get notified when a new token arrives
apiClient.registerAccessTokenListener(new AccessTokenListener() {
#Override
public void notify(BasicOAuthToken token) {
System.out.println("Got a fresh token: " + token.getAccessToken());
}
});
// following call exchanges the authorization code for an access code and updates
// the `Authorization: bearer <token>` header on the api client
apiClient.updateAccessToken();
I get an error saying the requested access token is null. Below is the error:
Error while requesting an access token. No accessTokenResponse object received, maybe a non HTTP 200 received?
Has anybody ever got the same one or could maybe someone tell me how to fix it.
I ran below code and its working fine for me, after getting code from the previous step in the URL:
public static void main(String[] args) {
String IntegratorKey = "[Your_Integrator_Key]";
String ClientSecret = "[Your_Secret_Key]";
String RedirectURI = "https://www.getpostman.com/oauth2/callback";//This REDIRECT_URI should match whats configured with IntegratorKey in your Sandbox account
String AuthServerUrl = "https://account-d.docusign.com";
String RestApiUrl = "https://demo.docusign.net/restapi";
ApiClient apiClient = new ApiClient(AuthServerUrl, "docusignAccessCode", IntegratorKey, ClientSecret);
apiClient.setBasePath(RestApiUrl);
apiClient.configureAuthorizationFlow(IntegratorKey, ClientSecret, RedirectURI);
Configuration.setDefaultApiClient(apiClient);
String code = "{ENTER_AUTH_CODE_FROM_PREVIOUS_STEP}";
apiClient.getTokenEndPoint().setCode(code);
apiClient.registerAccessTokenListener(new AccessTokenListener() {
#Override
public void notify(BasicOAuthToken token) {
System.out.println("Got a fresh token: " + token.getAccessToken());
}
});
apiClient.updateAccessToken();
}
To get the code returned by DocuSign from browser, you need to have a WEBApp to which DocuSign will redirect the browser, this same callback URL should be configured in your DS Sandbox's REDIRECT_URI, for instance if you WEBApp callback URL is http://locahost:8080/docusignapp/callback, then this should be added in REDIRECT_URI in IntegratorKey and same needs to be added when calling DS URL to authenticate a user. Once DS authenticates the user, it will redirect the browser to your APP url. On hitting your WEBApp then you need to read the URL and strip off the code part using Javascript, then run the step2 to get the access Token. A sample JS code to strip the code part is:
var vars = [], hash;
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
for(var i = 0; i < hashes.length; i++)
{
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
var authCode = vars["code"];
In the example which you share they did it using Standalone code, where you are manually copying the code part from the URL after authentication and running the step2.
I'm using the appengine endpoints-framework-v2 java sample. In this sample there is an endpoint called getUserEmailFirebase which should authenticated firebase user but it does not work
#ApiMethod(
path = "firebase_user",
httpMethod = ApiMethod.HttpMethod.GET,
authenticators = {EspAuthenticator.class},
issuerAudiences = {#ApiIssuerAudience(name = "firebase",
audiences = {"my-project-id"})}
)
public Email getUserEmailFirebase(User user) throws UnauthorizedException {
if (user == null) {
throw new UnauthorizedException("Invalid credentials");
}
Email response = new Email();
response.setEmail(user.getEmail());
return response;
}
I tried to send the request with an Authorization header inculding the firebase user id token but I'm getting 401 response, Also tried to add 'Bearer ' at the begining of the token but I get the same result. Even tried to follow this appengine guide Authenticating Users (Frameworks) without success.
The token is retrieved by AngularFire SDK using firebaseUser.getIdToken(), I have used the Firebase server sdk to authenticated users successfuly but it doesn't work using the above method
Can you help?