I'm using spring-boot-starter-oauth2-client to authenticate my user with Google. This works well and I can sign in and get valid access and refresh token as expected.
I'm creating the access token as such:
public class TokenServiceImpl implements TokenService {
private final OAuth2AuthorizedClientService clientService;
#Override
public GoogleCredentials credentials() {
final var accessToken = getAccessToken();
return getGoogleCredentials(accessToken);
}
private GoogleCredentials getGoogleCredentials(String accessToken) {
return GoogleCredentials
.newBuilder()
.setAccessToken(new AccessToken(accessToken, null))
.build();
}
private String getAccessToken() {
final var oauthToken = (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
return clientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName()).getAccessToken().getTokenValue();
}
}
The token is ultimately being used in the Google Photo API client as such
private PhotosLibraryClient getClient() {
final var settings =
PhotosLibrarySettings
.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(tokenService.credentials()))
.build();
return PhotosLibraryClient.initialize(settings);
}
The problem is that the token will expire after a short period and I'd like to refresh it to keep it active.
I'm unsure what pattern of methods I can use to do this, without having to write the entire OAuth flow (defeating the purpose of something like the Spring oauth2-client).
So far I have no other token/security/filter logic in my application.
Do I just need to write it all out manually, or is there another way I can do this?
The OAuth2AuthorizedClientManager will take care of refreshing your access token for you, assuming you get a refresh token along with your access token. The doco for OAuth2AuthorizedClientManager is at
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2client
When configuring your OAuth2AuthorizedClientManager, make sure you have included refreshToken in the OAuth2AuthorizedClientProvider...
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
You then use the OAuth2AuthorizedClientManager to get the access token. The sample from the spring doco is below...
#Controller
public class OAuth2ClientController {
#Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
#GetMapping("/")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
If the current accessToken has expired, this will automatically request a new accessToken using the previously obtained refreshToken.
Related
I've been asked to generate a token depending on the username that is asking for it. Now I'm creating a token just with a single subject but I don't know how to change the subject dinamically before creating the token depending on the body of the request.
This is what I've done so far to generate a token with a single subject:
The service class:
#Component
#RequiredArgsConstructor
public class JwtService {
#Value("${issuer}")
private String issuer;
#Value("${kid}")
private String keyId;
#Value("#{'${audience}'.split(',')}")
private List<String> audiences;
#Value("#{'${subject}'.split(',')}")
private List<String> subject;
private final JwtKeyProvider jwtKeyProvider;
public String generateToken() throws JoseException {
JwtClaims claims = new JwtClaims();
claims.setIssuer(issuer);
claims.setAudience(Lists.newArrayList(audiences));
claims.setExpirationTimeMinutesInTheFuture(60);
claims.setJwtId(keyId);
claims.setIssuedAtToNow();
claims.setNotBeforeMinutesInThePast(0);
claims.setSubject(subject);
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setHeader("typ", "JWT");
jws.setKey(jwtKeyProvider.getPrivateKey());
jws.setKeyIdHeaderValue(keyId);
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
String jwt = jws.getCompactSerialization();
return jwt;
}
}
And the controller:
#RestController
#RequiredArgsConstructor
public class JWTController {
private final JwtService jwtService;
#PostMapping("/getToken")
public ResponseEntity getJwt(#RequestBody JwtRequest request) throws JoseException {
return ResponseEntity.ok(
JwtResponse.builder()
.token(jwtService.generateToken())
.build()
);
}
}
I could do it doing like this:
#PostMapping("/getToken")
public ResponseEntity getJwt(#RequestBody JwtRequest request) throws JoseException {
return ResponseEntity.ok(
JwtResponse.builder()
.token(jwtService.generateToken(request.getUsername()))
.build()
);
}
}
But I don't want to send any parameters in the generateToken function as I would have to change a lot of code then.
To resume I want to assign to the subject the value of the username that is sent in the body. So is there a way in the JwtService class to receive that username and set as the subject after?
Thanks in advance!
First you need to put whitelist=user1,user2 in your application.properties, because sometimes names might trigger as system variables (for example username does)
Then in JWTController you need to check if not user equals, but contains in list
#Value("#{'${whitelist}'.split(',')}")
private List<String> whitelist;
#PostMapping("/getToken")
public ResponseEntity<?> getJwt(#RequestBody JwtRequest request) throws JoseException {
if(whitelist.contains(request.username())) {
return ResponseEntity.ok(
JwtResponse.builder()
.token(jwtService.generateToken(request.username()))
.build()
);
} else {
return ResponseEntity.ok("Invalid username");
}
}
In your JWTService you need to set JWT Subject to username which passed through whitelist
claims.setSubject(username);
And finally you need to do JSON request to server
{
"username": "user2"
}
I develop spring non-web service which has webflux basic authentication.
Its working and i am able to successfully use cURL to reach certain endpoints like:
curl -I --user user:password http://localhost:5051/service/v1/health
or
curl -I http://user:password#localhost:5051/service/v1/health
But now im trying to send post via other services which use OkHttp and Retrofit to communicate with my spring service.
This process is more complicated, in main apllication, the OkHttpCllient is created and then separate, Retrofit service client provider is called.
The main application:
httpUrl = url
.newBuilder()
.username(Username) // first approach
.password(Password)
{..}
.build();
ClientProvider
.getInstance(httpUrl, Username, Password)
.subscribeOn(Schedulers.io())
.subscribe(new ProfiledSingleObserver<Event>() {
#Override
public void profiledOnError(#NotNull Throwable e) {
LOG.error("Transport layer error", e);
resultFuture.completeExceptionally(e);
}
#Override
public void profiledOnSuccess(#NotNull Event event) {
resultFuture.complete(Collections.singleton(event));
}
});
public class ClientProvider {
private static HttpUrl httpUrl = null;
private static Service instance = null;
private ClientProvider() {
}
public static Service getInstance(final HttpUrl url, String username, String password) {
instance = Client.createService(url, HttpClient.getInstance(username, password));
return instance;
}
}
public static OkHttpClient getInstance(String username, String password) {
if (instance == null) {
synchronized (lock) {
instance = new OkHttpClient.Builder()
// .authenticator(new Authenticator() { // second approach
// #Override
// public Request authenticate(#Nullable Route route, #NotNull Response response) throws IOException {
// return response
// .request().newBuilder()
// .header("Authorization", Credentials.basic(username, password))
// .build();
// }
// })
// .addInterceptor(new BasicAuthInterceptor(username,password)) // third approach
.addInterceptor(createHttpBodyLoggingInterceptor())
.addInterceptor(createHttpBasicLoggingInterceptor())
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.dispatcher(createDispatcher())
.connectionPool(createConnectionPool())
.build();
}
}
return instance;
}
public class BasicAuthInterceptor implements Interceptor {
private final String credentials;
public BasicAuthInterceptor(String user, String password) {
this.credentials = Credentials.basic(user, password);
}
#NotNull
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request authenticatedRequest = request.newBuilder()
.header(HttpHeaders.AUTHORIZATION, credentials).build();
return chain.proceed(authenticatedRequest);
}
}
And the Retrofit service client provider:
public static Service createService(final HttpUrl baseUrl, final OkHttpClient okHttpClient) {
return createRetrofit(baseUrl, okHttpClient).create(Service.class);
}
protected static Retrofit createRetrofit(HttpUrl baseUrl, OkHttpClient client) {
return new Retrofit
.Builder()
.baseUrl(baseUrl)
.client(client)
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(JacksonConverterFactory.create())
.build();
}
I was taking 3 different approach to solve this problem, as u can see in the comments next to them.
First was just passing username and password via url to look something like:
http://user:password#localhost:5051/service/v1/health
but even if curl with this notation is working, authentication did not pass.
Second approach was with creating Authenticator in OkHttpClient.Builder().
Still the same results.
And third one was with creating Interceptor also in the OkHttpClient.
In this approach i was able to pass unit tests with stubbed server,
which was impossible in other solutions ( Status 404, Not found):
#BeforeClass
public static void setUpClass() {
serviceStubServer = new StubServer();
whenHttp(serviceStubServer)
.match(Condition.basicAuth("user", "pass"), Condition.post("/service/v1/health"))
.then(
Action.status(HttpStatus.OK_200),
);
serviceStubServer.run();
}
But still i was unable to send records to my Spring service via my main appplication( Status 401, Unauthorized).
My question is, whats the correct way to pass credentials thru OkHttp and Retrofit to be able to reach endpoints in Spring non-web, webflux basic authentication secure application?
Seems like the problem was with singleton approach to create OkHttpClient. This system was operating with two different Spring services and remembered one of the credentials, so he was unable to correctly reach second service.
To solve this problem i create second OkHttpClient class and use the "Interceptor" method to provide authorization data.
It might sound a bit strange. But i have a situation where my app needs to make an ROPC token request with userdetails to be taken from properties.
The call is triggered when there is a random incoming request, which is anonymous. But in the token request, while reaching AccessTokenProviderChain.class, token request fails.
org.springframework.security.authentication.InsufficientAuthenticationException: Authentication is required to obtain an access token (anonymous not allowed)
On debug, i found it is happening at below line.
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof AnonymousAuthenticationToken && !resource.isClientOnly()) {
throw new InsufficientAuthenticationException("Authentication is required to obtain an access token (anonymous not allowed)");
}
I see that AnonymousAuthenticationFilter sets principal as anonymousUser
Is this because the incoming request is anonymous and i am trying to make an ROPC token request ?
Is it because I have enabled #EnableMongoAuditing.
Is there a way to fix this issue other than creating a custom token request.
Code :
public OAuth2RestTemplate myappROPCRestTemplate() {
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(myappPasswordResourceDetails());
restTemplate.setAccessTokenProvider(getAccessTokenProvider());
if (proxyEnabled) {
restTemplate.setRequestFactory(getRequestFactoryWithProxy());
}
return restTemplate;
}
private AccessTokenProvider getAccessTokenProvider() {
ResourceOwnerPasswordAccessTokenProvider resourceOwnerPasswordAccessTokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
if (proxyEnabled) {
resourceOwnerPasswordAccessTokenProvider.setRequestFactory(getRequestFactoryWithProxy());
}
return new AccessTokenProviderChain(Collections.singletonList(resourceOwnerPasswordAccessTokenProvider));
}
private SimpleClientHttpRequestFactory getRequestFactoryWithProxy() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setOutputStreaming(false);
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, Integer.parseInt(proxyPort)));
requestFactory.setProxy(proxy);
return requestFactory;
}
private OAuth2ProtectedResourceDetails myappPasswordResourceDetails() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setAccessTokenUri(tokenUrl);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setUsername(username);
resource.setPassword(password);
resource.setClientAuthenticationScheme(AuthenticationScheme.form);
resource.setGrantType("password");
return resource;
}
I am building a userinfo endpoint on my Webflux rest api, how do I access the access_token passed in through the Authorization header in the rest call. Also need a similar endpoint to update the user.
All the examples I have found with latest spring 5/boot 2 are about securing a webapp.
#GetMapping("/api/user-info")
public Map userInfo(OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient authorizedClient = this.getAuthorizedClient(authentication);
Map userAttributes = Collections.emptyMap();
String userInfoEndpointUri = authorizedClient
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUri();
if (!StringUtils.isEmpty(userInfoEndpointUri)) {
// userInfoEndpointUri is optional for OIDC Clients
userAttributes = WebClient.builder()
.filter(oauth2Credentials(authorizedClient))
.build()
.get()
.uri(userInfoEndpointUri)
.retrieve()
.bodyToMono(Map.class)
.block();
}
return userAttributes;
}
private OAuth2AuthorizedClient getAuthorizedClient(OAuth2AuthenticationToken authentication) {
return this.authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(), authentication.getName());
}
private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) {
return ExchangeFilterFunction.ofRequestProcessor(
clientRequest -> {
ClientRequest authorizedRequest = ClientRequest.from(clientRequest)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue())
.build();
return Mono.just(authorizedRequest);
});
}
OAuth2AuthenticationToken object defined in the method is null which is understandable but not sure what else need configuring.
Thanks for your help.
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