Can't set up OAuth with Vkontakte via SpringBoot - java

Need help to implement login via Vkontakte OAuth.
According to spring-boot docs made this:
#Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(vkClientRegistration());
}
private ClientRegistration vkClientRegistration() {
return ClientRegistration.withRegistrationId("vk")
.authorizationUri("https://oauth.vk.com/authorize")
.clientId("######")
.clientSecret("######")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("friends", "groups")
.tokenUri("https://oauth.vk.com/access_token")
// .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
// .userNameAttributeName(IdTokenClaimNames.SUB)
// .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Vkontakte")
.build();
}
As a result: context starts, localhost:8080 redirects to authorization page.
But after clicking "Allow" button browser comes back to login page with error:
[invalid_token_response] An error occurred while attempting to
retrieve the OAuth 2.0 Access Token Response: 401 Unauthorized: [no
body]
I used VK API documentation from this:
https://dev.vk.com/api/access-token/authcode-flow-user
What should I correct to complete authorization flow?
Update
Debugging I found the reason of such behavior:
VK service returns access token like this:
{
"access_token":"ed0f46118dec0a5e6935ca198......",
"expires_in":86400,
"user_id":1111111,
"email":"1111222#mail.ru"
}
And it does not have property "token_type". As a result OAuth2AccessToken constructor fails.
public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
Set<String> scopes) {
super(tokenValue, issuedAt, expiresAt);
Assert.notNull(tokenType, "tokenType cannot be null");
this.tokenType = tokenType;
this.scopes = Collections.unmodifiableSet((scopes != null) ? scopes : Collections.emptySet());
}
There is a solution which detailed describes configuring flow at the link (in Russian)

Related

How to manage Scopes according to the User and not the Client?

I've been following this tutorial in order to create an Authentication Server, but I'm facing some problems regarding the concepts, I guess.
Look, when I register a Client in Repository, I have to define some parameters, like its id, secret, authentication method, grant types, redirection uris and scopes:
#Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("articles-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("articles.read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
When I'm back to my Resource Server, I find that my client was successfully logged in and it returns with an "articles.read" scope. Everything is fine here, supposing that I want to protect my endpoints with the Client's scope, but this is not my case.
In my situation, I want to protect my endpoints according to my User's role in database.
I'll give you an example, so you don't have to read the whole Baeldung's website:
I try to access: http://localhost:8080/articles.
It redirects to: http://auth-server:9000, where a Spring Security Login Form appears.
When you submit the proper credentials (which are compared from a database using the default Spring Security schema), it basically gets you back to: http://localhost:8080/articles.
Well, in that point, I have an Authorization Token with the Client scope, but not the logged User role.
Is there an standard way to configure my project to achieve this or, do I have to think of a creative way to do so?
Thank you in advance.
For role based authentication you should map authorities in Oauth token.
OAuth2AuthenticationToken.getAuthorities() is used for authorizing requests, such as in hasRole('USER') or hasRole('ADMIN').
For this you need to implement the userAuthoritiesMapper, something like this:
#Configuration
public class AppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper());
//.oidcUserService(this.oidcUserService());
super.configure(http);
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.containsClaim("role")){
String roleName = "ROLE_" + userInfo.getClaimAsString("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("role")){
String roleName = "ROLE_" + (String)userAttributes.get("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
});
return mappedAuthorities;
};
}
}

How to read email with proper rights in token

I am trying to upgrade an application (it should fetch emails from a mailbox every few minutes) from Microsoft EWS deprecated API to the new Graph API, but I am facing some issues.
This is my class for the connector :
public class O365graphApiConnector {
private final GraphServiceClient<Request> graphClient;
public O365graphApiConnector(String clientId, String username, String password) {
final UsernamePasswordCredential usernamePasswordCredential =
new UsernamePasswordCredentialBuilder()
.clientId(clientId)
.username(username)
.password(password)
.build();
final TokenCredentialAuthProvider tokenCredentialAuthProvider =
new TokenCredentialAuthProvider(usernamePasswordCredential);
graphClient=GraphServiceClient.builder()
.authenticationProvider(tokenCredentialAuthProvider)
.buildClient();
}
public User getUserProfile() {
return graphClient.me().buildRequest().get();
}
public MessageCollectionPage getOutlookEmails() {
return graphClient.me().messages().buildRequest().get();
}
}
I am using com.azure:azure-identity:1.4.2 and com.microsoft.graph:microsoft-graph:5.8.0.
I build the connector, passing the clientId, username and password. I am able to call getUserProfile , and I am getting something, so the authentication "works".
However, I get a 404 when calling getOutlookEmails :
SEVERE: Throwable detail:
com.microsoft.graph.http.GraphServiceException: Error code:
ResourceNotFound Error message: Resource could not be discovered.
GET https://graph.microsoft.com/v1.0/me/messages SdkVersion :
graph-java/v5.8.0
404 : Not Found [...]
When I run this in debug mode and intercept the token, it seems to be OK though : I have a bunch of rights that my admin has given to the applicative account :
"scp": "EWS.AccessAsUser.All Mail.Read Mail.Read.Shared Mail.ReadBasic Mail.ReadWrite
Mail.ReadWrite.Shared Mail.Send Mail.Send.Shared MailboxSettings.ReadWrite User.Read User.Read.All User.ReadWrite profile openid email"
This is part of what we see on the admin side (more rights were added after the screenshot was taken) :
My understanding is that this should be enough to get access to the emails of the given mailbox programmatically, but apparently, it's not.
Any idea of what I am missing ?
actually, the technical "user" I am using didn't really have a mailbox (despite the user name being an email address.. that confused me).
It had been given the permissions on the given mailbox I am interested in though, so the fix is simply to select the mailbox/user before retrieving the messages :
public MessageCollectionPage getOutlookEmailsFor(String mailbox) {
return graphClient.users(mailbox).messages().buildRequest().get();
}

Enabling OAuth2 with Feign for scheduled cross-service tasks

I'm trying to solve a puzzle with enabling OAuth2-based authentication for my Feign client that is used for cross-service communication.
In normal cases, when a user pushes a request through API, I'm able to take all authentication details needed from the Spring's SecurityContextHolder (as it normally does its job and fills all the details objects) and enhance Feign's Request as follows:
public class FeignAccessTokenRelyInterceptor implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(FeignAccessTokenRelyInterceptor.class);
#Override
public void apply(RequestTemplate template) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
String tokenValue = null;
if (auth.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
tokenValue = details.getTokenValue();
}
if (tokenValue == null) {
log.warn("Current token value is null");
return;
}
template.header("Authorization", "Bearer " + tokenValue);
}
}
}
However, when it comes to scheduled calls that are triggered inside the system, the SecurityContext is obviously empty. I'm filling it with UserInfoTokenServices by manually requesting the access token by client credentials flow beforehand and loading user details:
OAuth2Authentication authentication = userInfoTokenServices.loadAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
But such construction doesn't fill OAuth2Authentication.details, on which I rely to get an access token. I tried extending OAuth2AuthenticationDetails, but the only constructor requires HttpServletRequest which is hard to get inside a scheduled task, and making a dummy instance of it feels like a bad choice.
So far, I see the only adequate option to make a separate custom implementation of details holder and pass it to OAuth2Authentication along with the access token I have. And then pick it up in FeignAccessTokenRelyInterceptor.
The question
Maybe there are some other options where I can store my access token in the security context and reliably get it from there, in order not to produce new custom classes?
Will be glad for any help.
Some related links I've studied:
How to get custom user info from OAuth2 authorization server /user endpoint
Spring Boot / Spring Cloud / Spring Security: How to correctly obtain an OAuth2 Access Token in a scheduled task
Spring #FeignClient , OAuth2 and #Scheduled not working
How can I authenticate a system user for scheduled processes in Spring?
For the history, hope that'd help someone like me struggling with that.
I didn't find a better way than the initial one and made a custom InternalOAuth2Details to hold a token value obtained from Spring's OAuth services. Then, in the FeignAccessTokenRelyInterceptor I simply check if current details are InternalOAuth2Details and try to get a token value from it, as follows:
#Override
public void apply(RequestTemplate template) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
String tokenValue = null;
if (auth.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
tokenValue = details.getTokenValue();
} else if (auth.getDetails() instanceof InternalOAuth2Details) {
InternalOAuth2Details details = (InternalOAuth2Details) auth.getDetails();
tokenValue = details.getTokenValue();
}
if (tokenValue == null) {
log.warn("Current token value is null");
return;
}
template.header("Authorization", "Bearer " + tokenValue);
}
}
I bet it isn't the best solution, but it seems to work quite stable as of now.

Vertx - missing GET parameters while securing route using OAuth2

I followed this example of securing route using OAuth2 with GitHub provider: http://vertx.io/docs/vertx-web/java/#_oauth2authhandler_handler and it works fine, except missing GET parameters after request redirection.
My code:
public class MyVerticle extends AbstractVerticle {
#Override
public void start() throws Exception {
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
OAuth2Auth authProviderGitHub = GithubAuth.create(vertx, "<CLIENT_ID>", "<CLIENT_SECRET>");
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProviderGitHub, "http://localhost:8080/callback");
oauth2.setupCallback(router.route());
router.route("/protected/*").handler(oauth2);
Handler<RoutingContext> requestHandler = (routingContext) -> {
String paramValue = routingContext.request().getParam("param");
routingContext.response().end("PARAM: " + paramValue);
};
router.get("/endpoint").handler(requestHandler);
router.get("/protected/endpoint").handler(requestHandler);
server.requestHandler(router::accept).listen(8080);
}
}
I have two simple endpoints:
/endpoint // public, without protection
and
/protected/endpoint // protected with OAuth2
When I call from a browser /endpoint with
http://localhost:8080/endpoint?param=foo
it works as expected and return PARAM: foo, whereas when I call protected endpoint with
http://localhost:8080/protected/endpoint?param=foo
it correctly redirect me to GitHub login page, then return query to my handler but without GET parameters, so response from the endpoint is PARAM: null.
Any idea what I'm doing wrong?
On vert.x <= 3.4.2 only the path was being used for the redirect, the 3.5 series has been improved and can rely on the full uri, so your code will work on that version.

Google OAuth, handle a revoked authorization

I've been using Google OAuth to let users authorize access to the Calendar Service for my Web Application. After a successful 3-legged auth flow, I was storing all user's credentials in a common file on the app Server. The next time the app needs to use the service, it will check if the credentials exist, and if yes, it will assume they are valid
code works like that
#Override
public void _authorize(String userId) throws IOException {
// Check if user has already authorised the service.
Credential credents = flow.loadCredential(userId);
// Checking if the given user is not authorized
if (credents == null) {
//Create credentials now. user will be redirected to authorise
try {
//Creating a LocalServer Receiver
// Getting the redirect URI
// Creating a new authorization URL
// Setting the redirect URI
// Building the authorization URL
// Receiving authorization code
// Exchanging it for an access token
// Storing the credentials for later access
credents = flow.createAndStoreCredential(response, id);
} finally {
// Releasing resources
}
} else {
// Assume the credentials are valid. so there's nothing left to do here, let's get that client
//Update: Nooooooot! the user might have revoked the authorization, so credents != null BUT they are invalid
//TODO: handle an Exception here, and manage the revoked credentials
}
// Setting up the calendar service client
client = new com.google.api.services.calendar.Calendar.Builder(httpTransport, jsonFactory, credents).setApplicationName(APPLICATION_NAME)
.build();
}
This works fine, as long as the user never changes his mind. But if the user decides to manually revoke the authorization using the Google Account security options, the com.google.api.services.calendar.Calendar retrieval will Fail.
My question is :
Is there a way to check if the credentials are still valid, before trying to use them ?
Else, I can only guess that the failure to get the client object, is the only way to have my portal realize that the credentials are no more valid ?
What should I do about the invalid/revoked credentials ? should I just call flow.createAndStoreCredential and they are going to be overwritten? Or do I have to delete the old ones first ? (how ?)
You can use the refreshToken() method for this. See example:
// Fetch credential using the GoogleAuthorizationCodeFlow
GoogleAuthorizationCodeFlow authorizationCodeFlow;
Credential credential = authorizationCodeFlow.loadCredential(userId);
if (credential != null) {
try {
// refresh the credential to see if the refresh token is still valid
credential.refreshToken();
System.out.println("Refreshed: expires in: " + credential.getExpiresInSeconds());
} catch (TokenResponseException e) {
// process exception here.
// This will catch the Exception.
// This Exception contains the HTTP status and reason etc.
// In case of a revoke, this will throw something like a 401 - "invalid_grant"
return;
}
} else {
// No credential yet known.
// Flow for creating a new credential here
}
EDIT
If you indeed have an invalid refresh token and you want to renew it, then you need to repeat the steps that you did in the first place to get the credentials. So:
genererate a new authorization URL
redirect the user to it
user accepts the consent screen
catch the authorization code from the redirect back to your app
request a new token from Google using the authorization code
create and store a new Credential using the response from Google
No need to delete the old credential. But if you want to explicitly do so, it is possible.
Something like:
// This userId is obviously the same as you used to create the credential
String userId = "john.doe";
authorizationCodeFlow.getDataStore().delete(userId);
You can use the endpoint https://www.googleapis.com/oauth2/v1/tokeninfo to determine if an OAuth2 token is still valid. More information is available in the OAuth2 guide.
Answer to the first question:
When using the Service object for retrieving calendar items from Google Calendar, the token are automatically verified. When they are invalid, they will be refreshed automatically, and stored in the datastore you provided to the flow.
this can also be done manually. A token is valid for 3600 seconds (one hour). When retrieving a token you get this value with the timestamp when it was issued. You could manually determine if a token is valid. If it is not valid call the following async method.
await credents.RefreshtokenAsync(CancellationToken.None);
This function gets you fresh tokens, and stores them in the datastore you provided.
You could check token with tokeninfo and if token is not valid:
- remove credential from datastore
- invoke new auth
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
UserService userService = UserServiceFactory.getUserService();
if (userService.isUserLoggedIn()) {
User user = userService.getCurrentUser();
log.info(String.format("LoggedUser: %s %s", user.getEmail(), user.getUserId()));
Credential credential = this.getCredential();
Tokeninfo tokenInfo = OAuth2Utils.getTokenInfo(credential, null);
if (tokenInfo != null)
log.info(String.format("Token expires in: %d", tokenInfo.getExpiresIn()));
else {
OAuth2Utils.deleteCredential(user.getUserId());
response.sendRedirect(request.getRequestURI()); // recall this servlet to require new user authorization
return;
}
}
public static Tokeninfo getTokenInfo(Credential credential, String accessToken) {
Oauth2 service = new Oauth2.Builder(new NetHttpTransport(), Constant.JSON_FACTORY, credential).setApplicationName(Constant.APP_NAME).build();
Tokeninfo tokenInfo = null;
try {
tokenInfo = service.tokeninfo().setAccessToken( accessToken == null ? credential.getAccessToken() : accessToken ).execute();
} catch (IOException e) {
log.warning("An error occurred: " + e);
}
return tokenInfo;
}

Categories

Resources