Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Client Credentials Authentication #72

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,52 @@
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.Assert;

/**
* An {@link AuthenticationProvider} implementation that validates {@link OAuth2ClientAuthenticationToken}s.
*
* @author Joe Grandja
* @author Patryk Kostrzewa
*/
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
private RegisteredClientRepository registeredClientRepository;
private final RegisteredClientRepository registeredClientRepository;

/**
* @param registeredClientRepository
* the bean to lookup the client details from
*/
public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.registeredClientRepository = registeredClientRepository;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return authentication;
String clientId = authentication.getName();
if (authentication.getCredentials() == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
}

RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
// https://tools.ietf.org/html/rfc6749#section-2.4
if (registeredClient == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
}

String presentedSecret = authentication.getCredentials()
.toString();
if (!registeredClient.getClientSecret()
pkostrzewa marked this conversation as resolved.
Show resolved Hide resolved
.equals(presentedSecret)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
}

return new OAuth2ClientAuthenticationToken(registeredClient);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;

import java.util.Collections;

/**
* @author Joe Grandja
* @author Patryk Kostrzewa
*/
public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
pkostrzewa marked this conversation as resolved.
Show resolved Hide resolved
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
Expand All @@ -44,11 +44,11 @@ public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient) {

@Override
public Object getCredentials() {
return null;
return this.clientSecret;
}

@Override
public Object getPrincipal() {
return null;
return this.clientId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;

import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
* Converts from {@link HttpServletRequest} to {@link OAuth2ClientAuthenticationToken} that can be authenticated.
*
* @author Patryk Kostrzewa
*/
public class DefaultOAuth2ClientAuthenticationConverter implements AuthenticationConverter {

private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";

@Override
public OAuth2ClientAuthenticationToken convert(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);

if (header == null) {
return null;
}

header = header.trim();
if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
return null;
}

if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
}

byte[] decoded;
try {
byte[] base64Token = header.substring(6)
.getBytes(StandardCharsets.UTF_8);
decoded = Base64.getDecoder()
.decode(base64Token);
} catch (IllegalArgumentException e) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
}

String token = new String(decoded, StandardCharsets.UTF_8);
String[] credentials = token.split(":");
if (credentials.length != 2) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN));
}
return new OAuth2ClientAuthenticationToken(credentials[0], credentials[1]);
Copy link
Contributor

@anoopgarlapati anoopgarlapati May 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkostrzewa cc: @jgrandja
As per RFC 6749 - section 2.3.1, the client identifier and client password are further encoded using application/x-www-form-urlencoded encoding algorithm and then used as username and password in HTTP Basic Authentication Scheme.

The client identifier is encoded using the
"application/x-www-form-urlencoded" encoding algorithm per
Appendix B, and the encoded value is used as the username; the client
password is encoded using the same algorithm and used as the
password.

There is an issue already logged in legacy oauth2 project spring-attic/spring-security-oauth#1826 and we should implement this according to specification in this project.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anoopgarlapati
Although RFC 6749 - section 2.3.1, indeed does say:

The client identifier is encoded using the
"application/x-www-form-urlencoded" encoding algorithm per
Appendix B, and the encoded value is used as the username; the client
password is encoded using the same algorithm and used as the
password.

It further states:

Including the client credentials in the request-body using the two parameters is NOT RECOMMENDED and SHOULD be limited to clients unable to directly utilize the HTTP Basic authentication scheme (or other password-based HTTP authentication schemes). the parameters can only be transmitted in the request-body and MUST NOT be included in the request URI.

The authorization server MUST require the of TLS as described in Section 1.6 when sending requests using password authentication.

Since this client authentication method involves a password, the authorization server MUST protect any endpoint utilizing it against brute force attacks.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those sections do not contradict each other. The encoding needs to be applied when using Basic authentication.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,21 @@
*/
package org.springframework.security.oauth2.server.authorization.web;

import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -26,15 +38,130 @@

/**
* @author Joe Grandja
* @author Patryk Kostrzewa
*/
public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
private AuthenticationManager authenticationManager;

public static final String DEFAULT_FILTER_PROCESSES_URL = "/oauth2/token";
private final AuthenticationManager authenticationManager;
private final RequestMatcher requestMatcher;
private final OAuth2ErrorHttpMessageConverter errorMessageConverter = new OAuth2ErrorHttpMessageConverter();
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler authenticationFailureHandler;
private AuthenticationConverter authenticationConverter = new DefaultOAuth2ClientAuthenticationConverter();

/**
* Creates an instance which will authenticate against the supplied
* {@code AuthenticationManager}.
*
* @param authenticationManager
* the bean to submit authentication requests to
*/
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_FILTER_PROCESSES_URL);
}

/**
* Creates an instance which will authenticate against the supplied
* {@code AuthenticationManager}.
*
* <p>
* Configures default {@link RequestMatcher} verifying the provided endpoint.
*
* @param authenticationManager
* the bean to submit authentication requests to
* @param filterProcessesUrl
* the filterProcessesUrl to match request URI against
*/
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager, String filterProcessesUrl) {
this(authenticationManager, new AntPathRequestMatcher(filterProcessesUrl, "POST"));
}

/**
* Creates an instance which will authenticate against the supplied
* {@code AuthenticationManager} and custom {@code RequestMatcher}.
*
* @param authenticationManager
* the bean to submit authentication requests to
* @param requestMatcher
* the {@code RequestMatcher} to match {@code HttpServletRequest} against
*/
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager,
RequestMatcher requestMatcher) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.authenticationManager = authenticationManager;
this.requestMatcher = requestMatcher;
this.authenticationSuccessHandler = this::defaultAuthenticationSuccessHandler;
this.authenticationFailureHandler = this::defaultAuthenticationFailureHandler;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

if (this.requestMatcher.matches(request)) {
Authentication authentication = this.authenticationConverter.convert(request);
if (authentication == null) {
filterChain.doFilter(request, response);
return;
}
try {
final Authentication result = this.authenticationManager.authenticate(authentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, result);
} catch (OAuth2AuthenticationException failed) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
pkostrzewa marked this conversation as resolved.
Show resolved Hide resolved
return;
}
}
filterChain.doFilter(request, response);
}

/**
* Used to define custom behaviour on a successful authentication.
*
* @param authenticationSuccessHandler
* the handler to be used
*/
public final void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}

/**
* Used to define custom behaviour on a failed authentication.
*
* @param authenticationFailureHandler
* the handler to be used
*/
public final void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}

/**
* Used to define custom {@link AuthenticationConverter}.
*
* @param authenticationConverter
* the converter to be used
*/
public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}

private void defaultAuthenticationSuccessHandler(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {

SecurityContextHolder.getContext()
.setAuthentication(authentication);
}

private void defaultAuthenticationFailureHandler(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException {

SecurityContextHolder.clearContext();
pkostrzewa marked this conversation as resolved.
Show resolved Hide resolved
this.errorMessageConverter.write(((OAuth2AuthenticationException) failed).getError(),
MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
}
}
Loading