I can not figure out how to use thoso two together. After user is authenticated, I have OAuth2AuthorizedClient in the context which then I can use like:
String clientRegistrationId = auth.getAuthorizedClientRegistrationId();
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId, auth.getName());
accessToken = client.getAccessToken().getTokenValue();
So I can use accessToken in my application. But this is manual interceptor that I inject into RestTemplate. Also if I do this way than I have to handle all refreshToken logic with another interceptor. Here then comes OAuth2RestTemplate, it handles all accessToken, refresToken stuff which I don't need interceptors. But to use it I need OAuth2ProtectedResourceDetails:
public OAuth2RestTemplate(OAuth2ProtectedResourceDetails resource) {
this(resource, new DefaultOAuth2ClientContext());
}
which I don't if I use spring.security.oauth2.client.registration style. Because AuthorizationCodeResourceDetails created only if security.oauth2.client style is used.:
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
example-authorization-code:
id: demo-webapp-oidc-code
client-id: demo-webapp-oidc-code
client-secret: secret
client-name: demo-webapp-oidc-code
provider: authServer
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code
client-authentication-method: post
scope: openid, address
provider:
authServer:
token-uri: http://localhost:8095/authServer/oidc/token
authorization-uri: http://localhost:8095/authServer/oidc/authorize
user-info-uri: http://localhost:8095/authServer/oidc/profile
jwk-set-uri: http://localhost:8095/authServer/oidc/jwks
user-name-attribute: id
This is how It accepts:
#Bean
#ConfigurationProperties(prefix = "security.oauth2.client")
#Primary
public AuthorizationCodeResourceDetails oauth2RemoteResource() {
return new AuthorizationCodeResourceDetails();
}
I can create it manually but than things getting complicated. Lets imagine somehow I created resourceDetails, than I create OAuth2RestTemplate with fllowing,
new OAuth2RestTemplate( resourceDetails, defaultOAuth2ClientContext);
which means I need to set new DefaultOAuth2ClientContext:
public DefaultOAuth2ClientContext(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.accessTokenRequest = new DefaultAccessTokenRequest();
}
which requires Oauth2AccessToken. But I have core.OAuth2AccessToken in my context ( OAuth2AuthorizedClient authorizedClient.getAccessToken), while it requires common.OAuth2AccessToken.
Related
How I can enable logs to see actual http queries between my Spring Boot app verifying the tokens and the oauth2 server?
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://myhost/idm-services/auth/realms/myrealm
registration:
keycloak:
client-id: myclientid
logging:
level:
org:
springframework:
web:
filter:
CommonsRequestLoggingFilter: DEBUG
security:
oauth2: TRACE
I have this enabled, but I don't see any actual logs to oauth2.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated().and()
.oauth2Login().userInfoEndpoint().oidcUserService(this.oidcUserService());
}
#Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
log.debug("User "+oidcUser.toString());
log.debug("UserInfo "+oidcUser.getUserInfo().toString());
log.debug("UserClaims "+oidcUser.getClaims().toString());
log.debug("UserToken "+oidcUser.getIdToken().toString());
final Map<String, Object> claims = oidcUser.getClaims();
final JSONArray groups = (JSONArray) claims.get("org_access");
final Set<GrantedAuthority> mappedAuthorities = groups.stream()
.map(role -> new SimpleGrantedAuthority(""+role))
.collect(Collectors.toSet());
return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
};
}
there is the security configuration. I'd like to debug the token info transformation in oidcUserService, but the debug lines are not hit during the debug.
No way to trace in org.springframework.security.oauth2.server.resource.authentication
There are just no logging entries there. The only known stuff is to put all logging to trace, then you will see for example HttpConnection calls to ouath servers, that let you clarify what is going on. It is impossible to trace the tokens and data transmitted, so on the stage where you have no option to debug through the remote port, you have no option to understand what was the problem.
I am using the swagger with springdoc-openapi-ui-1.4.3
#SecurityRequirement(name = "security_auth")
public class ProductController {}
Setting the security schema
#SecurityScheme(name = "security_auth", type = SecuritySchemeType.OAUTH2,
flows = #OAuthFlows(authorizationCode = #OAuthFlow(
authorizationUrl = "${springdoc.oAuthFlow.authorizationUrl}"
, tokenUrl = "${springdoc.oAuthFlow.tokenUrl}",scopes = {
#OAuthScope(name = "IdentityPortal.API", description = "IdentityPortal.API")})))
public class OpenApiConfig {}
Security config
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {// #formatter:off
http
.authorizeRequests()
.antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
.permitAll()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
With dependencies
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springdoc:springdoc-openapi-ui:1.4.3'
implementation 'org.springdoc:springdoc-openapi-security:1.4.3'
implementation "org.springframework.boot:spring-boot-starter-security"
Config setting
spring:
profiles:
active: dev
####### resource server configuration properties
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://localhost:5001
jwk-set-uri: https://localhost:5001/connect/token
springdoc:
swagger-ui:
oauth:
clientId: Local
usepkcewithauthorizationcodegrant: true
oAuthFlow:
authorizationUrl: https://localhost:5001
tokenUrl: https://localhost:5001/connect/token
In the swagger UI, the clientId is empty and client secret is present, for authorization code + PKCE flow client secret should not present
Your property syntax
usepkcewithauthorizationcodegrant
is not correct:
Here is the right property for PKCE:
springdoc.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true
To fill, the client id, just use:
springdoc.swagger-ui.oauth.client-id=yourSPAClientId
For your remark of the existing secret filed that can be hidden. This looks like an enhancement on the swagger-ui.
You should submit an enhancement on the swagger-ui project:
https://github.com/swagger-api/swagger-ui/
it is some time since you asked the question but I will respond for others information. The major issue is the misleading implementation of the UI. You are forced to use the authorization code flow in the configuration because the authorization code with PKCE is missing. So you must use the authorization code (because you need to provide authorization and token url) and place a dummy secret into the yaml. Example below.
#SecurityScheme(name = "security_auth", type = SecuritySchemeType.OAUTH2,
flows = #OAuthFlows(authorizationCode = #OAuthFlow(
authorizationUrl = "${springdoc.oAuthFlow.authorizationUrl}"
, tokenUrl = "${springdoc.oAuthFlow.tokenUrl}")))
public class OpenApiConfig {}
If you want to use PKCE instead of the pure implicit set proper attribute (as #brianbro pointed) and a dummy secret as:
springdoc.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true
springdoc.swagger-ui.oauth.clent-secret=justFillerBecausePKCEisUsed
As last not least if you want to prefill the client_id use configuration:
springdoc.swagger-ui.oauth.client-id=YourClientId
I'm trying to generate an access token from a custom corporate oauth 2 authorization server with Resource Owner Password Credentials Flow.
See https://www.rfc-editor.org/rfc/rfc6749#section-4.3
This server only generate an access token if receive the following parameters:
POST https://custom_corporate_server/auth/oauth/v2/token
Header
idp: 99
Body
grant_type: password
scope: my_scope
client_id: 00******-****-****-****-**********99
client_secret: 00******-****-****-****-**********99
username: my_user
password: my_password
Their configuration requires additional header custom parameter: idp - should be a numeric.
I'm using Spring Boot 2.3.0 and Spring Security 5.3.2.
I followed the link bellow to build my test example:
https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#using-the-access-token-2
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.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;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
I was unable to pass this parameter in the header to the authorization server. How to do this is my main dilemma today.
Have look at this article, it explains all kinds of customizations to authorization and token requests. In your case, the section about token request extra parameters, seems to describe exactly what you need.
You could do something like this:
public class CustomRequestEntityConverter implements Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> {
private OAuth2PasswordGrantRequestEntityConverter defaultConverter;
public CustomRequestEntityConverter() {
defaultConverter = new OAuth2PasswordGrantRequestEntityConverter();
}
#Override
public RequestEntity<?> convert(OAuth2PasswordGrantRequest req) {
RequestEntity<?> entity = defaultConverter.convert(req);
MultiValueMap<String, String> params = entity.getHeaders();
params.add("idp", "99");
return new RequestEntity<>(params, entity.getHeaders(), entity.getMethod(), entity.getUrl());
}
}
I've currently set up a Spring Cloud Gateway Reverse Proxy with the intention of:
a) Handling Authentication of multiple OAuth/OIDC providers, including obtaining a Token
b) Look up the details from the provider locally, ensuring that the OAuth user x Oauth provider combination are authorised
c) If authorised look up the Grants/Permissions, and forward the request to SCG, with a JWT containing details of the authorised principal.
d) If not authorised display a page displaying pertinent details from the OAuth2 Auth, and explain that they are not authorised.
I have achieved most steps, but I am having trouble incorporating step c) into Spring Security Webflux
What I want to do is take the OAuth2AuthenticationToken obtained from the Authentication exchange, perform the lookup in step, and return a
bespoke Prinicipal based on results.
This would then be used via code to either trigger the SCG behaviour, or display the page.
spring-boot.version>2.1.6.RELEASE
spring-cloud.version>Greenwich.SR2
My problem is I don't know the best way of doing this.
Use some hook in the OAuth2 client to perform the extra auth steps. I may need to return an OAuth2Principal in this case
Add an extra security filter into the chain after the authentication.
This would replace the OAuth2Principal with my own prncipal. I'm not sure whether it is legal to replace a Principal after it has been authenticated, possibly removing the authentication status
Writing a custom AuthN Provider that would proxy to the OAuth client, and once competed run it's own logic before signalling that it is authenticated. This seems a complicated approach, and I'm not sure what classes I would use for this.
I've read the Spring Security documentation, and understand the general architecture of Spring Security, but cannot work out the best way of solving this.
This is my spring security filter logic
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
#Profile("oauth")
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return addAuthZ(http)
.oauth2Login()
.and().build();
}
private ServerHttpSecurity addAuthZ(ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated().and();
}
}
And here is the config, I am using sample OAuth2 providers Google and Facebook and a custom OAuth2 provider provided using CAS
spring:
security:
oauth2:
client:
registration:
google:
client-id: SET_ME
client-secret: SET_ME
facebook:
client-id: SET_ME
client-secret: SET_ME
sgd-authn:
provider: sgd-authn
client-id: SET_ME
client-secret: SET_ME
scope: openid
client-authentication-method: secret
authorization-grant-type: authorization_code
#redirect-uri: "{baseUrl}/oauth2/
redirect-uri-template: "{baseUrl}/{action}/oauth2/code/{registrationId}"
provider:
# These are needed for talking to CAS OIDC
sgd-authn:
authorization-uri: ${cas.url}/oidc/authorize
token-uri: ${cas.url}/oidc/accessToken
jwk-set-uri: ${cas.url}/oidc/jwks
user-info-uri: ${cas.url}/oidc/profile
user-name-attribute: sub
Well I managed to get something working using option 2, adding a filter after authentication.
Write a filter that takes in an Authentication (OAuth), runs it through some logic, and returns a new Authentication object that is instanceof a specific superclass
Arrange for this to return isAuthenticated = false if we could not look up the OAuth2 details.
Write a bespoke handler for NotAuthenticated
The filter is registered using something like this:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http,
WebFilter proxyAuthFilter
) {
return http.addFilterAt(proxyAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION); // Do configuration ...
}
and the filter is something like this:
public class ProxyAuthFilter implements WebFilter {
static private Logger LOGGER = LoggerFactory.getLogger(ProxyAuthFilter.class);
private final AuthZClientReactive authZClient;
public ProxyAuthFilter(AuthZClientReactive authZClient) {
this.authZClient = authZClient;
}
/**
* Process the Web request and (optionally) delegate to the next
* {#code WebFilter} through the given {#link WebFilterChain}.
*
* #param exchange the current server exchange
* #param chain provides a way to delegate to the next filter
* #return {#code Mono<Void>} to indicate when request processing is complete
*/
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.flatMap(this::transform)
.then(chain.filter(exchange));
}
private Mono<Authentication> transform(SecurityContext securityContext) {
Authentication authentication = securityContext.getAuthentication();
if (authentication.isAuthenticated()) {
return authenticate(authentication)
.map(a -> {
securityContext.setAuthentication(a);
return a;
});
} else {
LOGGER.info("ProxyFilter - not authenticated {}", authentication);
return Mono.just(authentication);
}
}
// Runs the chain, then returns a Mono with the exchange object which completes
// when the auth header is added to the request.
private Mono<Authentication> authenticate(Authentication authentication) {
LOGGER.info("ProxyFilter - Checking authorisation for {}", authentication);
Mono<AuthTokenInfo.Builder> authInfo = null;
// Catch if this filter runs twice
if (authentication instanceof ProxyAuthentication) {
LOGGER.info("ProxyAuthentication already found");
return Mono.just(authentication);
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
// If lookup is successful, returns instance of ProxyAuthentication
return getUsernamePasswordAuth((UsernamePasswordAuthenticationToken) authentication);
} else if (authentication instanceof OAuth2AuthenticationToken) {
// If lookup is successful, returns instance of ProxyAuthentication
return getOAuthAuth((OAuth2AuthenticationToken) authentication);
} else {
LOGGER.info("Unknown principal {}", authentication);
// Signals a failed authentication, can be picked up by error page
// to display bespoke information
return Mono.just(new ProxyAuthenticationNotAuthenticated(
authentication, ProxyAuthenticationNotAuthenticated.Reason.UnknownAuthenticationType)
);
}
}
Am trying to use Spring Secruity's OAuth API to obtain an access token from an externally published API within a Spring MVC 4 based Web Services (not Spring Boot).
This curl command works (and its contents are all that I need to obtain an access token):
curl -X POST \
https://api.app.com/v1/oauth/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d'grant_type=client_credentials&client_id=bcfrtew123&client_secret=Y67493012'
Spring Security OAuth API:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
My code to obtain access token:
#RequestMapping(value = "/getAccessToken", method = RequestMethod.POST, consumes="application/x-www-form-urlencoded")
public OAuth2AccessToken getAccessToken(#RequestParam(value="client_id", required=true) String clientId, #RequestParam(value="client_secret", required=true) String clientSecret) throws Exception {
String tokenUri = "https://api.app.com/v1/oauth/token";
ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
resourceDetails.setAccessTokenUri(tokenUri);
resourceDetails.setClientId(clientId);
resourceDetails.setClientSecret(clientSecret);
resourceDetails.setGrantType("client_credentials");
resourceDetails.setScope(Arrays.asList("read", "write"));
DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();
oauth2RestTemplate = new OAuth2RestTemplate(resourceDetails, clientContext);
OAuth2AccessToken token = oauth2RestTemplate.getAccessToken();
return token;
}
When I invoke the getAccessToken call from my local tomcat instance:
access_denied
error_description=Unable to obtain a new access token for resource 'null'.
The provider manager is not configured to support it.
Am suspecting the reason is that my Http Header's Content-Type is not set for
application/x-www-form-urlencoded
How do I do set that for:
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
If you notice, I am trying to set in inside the #RequestMapping and don't think that its working:
#RequestMapping(consumes="application/x-www-form-urlencoded")
The http headers for accessing the token in Oauth2Restemplate in case of Client credentials are set in below method of ClientCredentialsAccessTokenProvider (since grant type is client credentials)
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
return retrieveToken(request, resource, getParametersForTokenRequest(resource), new HttpHeaders());
}
We can set the http headers by having new custom Access token provider for client credentials and modifying the method as follows:
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, AccessDeniedException, OAuth2AccessDeniedException {
ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
HttpHeaders headers1 = new HttpHeaders();
headers1.add("Content-Type", "application/x-www-form-urlencoded");
return retrieveToken(request, resource, getParametersForTokenRequest(resource), headers1);
}
You can keep the class same as ClientCredentialsAccessTokenProvider and add just the header lines.
Last step will be to set this new class as access token in configuration of Oauth2RestTemplate.
oauth2RestTemplate.setAccessTokenProvider(new ClientCredentialsCustomAccessTokenProvider());
This worked for me!
Here's another variation on the answer just to override the default Accept Header interceptor using a Lambda expression:
#Bean
protected RestTemplate restTemplate() {
return new RestTemplate() {
#Override
public <T> RequestCallback acceptHeaderRequestCallback(Class<T> responseType) {
return request -> {
request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
};
}
};
}
If you are using Spring boot mention the authentication scheme as form, it will solve the issue.
security:
oauth2:
client:
clientAuthenticationScheme: form