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

Provide support for OAuth 2.0 Login #4257

Merged
merged 1 commit into from
Apr 28, 2017

Conversation

jgrandja
Copy link
Contributor

This PR provides support for authenticating using an external OAuth 2.0 or OpenID Connect 1.0 Provider. For example, login using Google, Facebook, GitHub, etc.

This PR addresses #3907

@jgrandja jgrandja added the in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) label Mar 21, 2017
@jgrandja jgrandja added this to the 5.0.0.M1 milestone Mar 21, 2017
@jgrandja jgrandja requested a review from rwinch March 21, 2017 17:02
@rwinch
Copy link
Member

rwinch commented Mar 22, 2017

General

  • Rebase and resolve build.gradle conflict
  • Do all assertions at the top of the constructor rather than assert, assign, assert, assign, ...
  • I'm confused why AuthorizationRequestAttributes is in oauth2-core and AuthorizationRequestRepository is in oauth2-client
  • rather than final methods make class final

spring-security-oauth2-core

  • I think that AbstractToken should probably have constructs on it for expiration. If not, we need to have expiration added to RefreshToken
  • the openid package should be a child of oauth2

spring-security-oauth2-client

oauth2login

  • Please add a README with directions on setup oauth2login Where to go to get client-id, client-secret, etc for each provider.
  • email is displayed as empty for github client
  • Running in STS with Buildship gives an error
java.lang.NoSuchMethodError: ch.qos.logback.core.util.Loader.getResourceOccurrenceCount(Ljava/lang/String;Ljava/lang/ClassLoader;)Ljava/util/Set;
	at ch.qos.logback.classic.util.ContextInitializer.multiplicityWarning(ContextInitializer.java:160)
	at ch.qos.logback.classic.util.ContextInitializer.statusOnResourceSearch(ContextInitializer.java:183)
	at ch.qos.logback.classic.util.ContextInitializer.getResource(ContextInitializer.java:141)
	at ch.qos.logback.classic.util.ContextInitializer.findURLOfDefaultConfigurationFile(ContextInitializer.java:130)
	at ch.qos.logback.classic.util.ContextInitializer.autoConfig(ContextInitializer.java:148)
	at org.slf4j.impl.StaticLoggerBinder.init(StaticLoggerBinder.java:85)
	at org.slf4j.impl.StaticLoggerBinder.<clinit>(StaticLoggerBinder.java:55)
	at org.slf4j.LoggerFactory.bind(LoggerFactory.java:150)
	at org.slf4j.LoggerFactory.performInitialization(LoggerFactory.java:124)
	at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:412)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:357)
	at org.apache.commons.logging.impl.SLF4JLogFactory.getInstance(SLF4JLogFactory.java:156)
	at org.apache.commons.logging.impl.SLF4JLogFactory.getInstance(SLF4JLogFactory.java:132)
	at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:274)
	at org.springframework.boot.SpringApplication.<clinit>(SpringApplication.java:190)
	at org.springframework.security.samples.OAuth2LoginApplication.main(OAuth2LoginApplication.java:31)

Copy link
Member

@rwinch rwinch left a comment

Choose a reason for hiding this comment

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

I'm submitting this for now and will continue the review later. This way you can iterate on it.

ClientRegistrationRepository clientRegistrationRepository,
AuthorizationRequestUriBuilder authorizationUriBuilder) {

Assert.notNull(filterProcessingUri, "filterProcessingUri cannot be null");
Copy link
Member

Choose a reason for hiding this comment

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

For consistency with the Spring code base...Move all assertions to the top and then assign. Do not do assert, assign, assert, assign


@Override
public final void afterPropertiesSet() {
Assert.notEmpty(this.clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
Copy link
Member

Choose a reason for hiding this comment

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

Generally, try to avoid afterProperties set unless you have to because validation only happens if people remember to invoke this method. Have the clientRegistrationRepository constructor validate that it has registrations and remove this method.

*
* @author Joe Grandja
*/
public class AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than all the final methods make the whole class final

import org.springframework.security.core.Authentication;

/**
* Root exception for all OAuth2-related general errors.
Copy link
Member

Choose a reason for hiding this comment

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

As explained in the note this isn't really true. The name of the class needs refined too

@@ -0,0 +1,41 @@
/*
Copy link
Member

Choose a reason for hiding this comment

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

The only place this is used is within a private method. I think this should be removed

try {
redirectUri = this.authorizationUriBuilder.build(authorizationRequestAttributes);
} catch (URISyntaxException ex) {
logger.error("An error occurred building the Authorization Request: " + ex.getMessage(), ex);
Copy link
Member

Choose a reason for hiding this comment

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

Do not swallow exceptions. If this code is kept, the user would just get an IllegalArgumentException on 134 but wouldn't know it was due to the URISyntaxException. Instead we should let a RuntimeException be thrown. See comment on the builder api too

response.sendError(HttpServletResponse.SC_BAD_REQUEST, failed.getMessage());
}

private String normalizeUri(String uri) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you use UriComponentsBuilder instead of writing the logic yourself?

/**
* @author Joe Grandja
*/
public interface AuthorizationGrantTokenExchanger<T extends AuthorizationGrantAuthenticationToken> {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason this is generic vs just always using AuthorizationCodeGrantAuthenticationToken

Copy link
Contributor Author

@jgrandja jgrandja Mar 29, 2017

Choose a reason for hiding this comment

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

There will be different implementations of AuthorizationGrantTokenExchanger for each of the grant types authorization_code, client_credentials and resource_owner_password grant types.

For example, the client_credentials grant type would be ClientCredentialsTokenExchanger which would be associated with it's specific type of AuthorizationGrantAuthenticationToken, for example, ClientCredentialsAuthenticationToken

/**
* @author Joe Grandja
*/
public interface AuthorizationRequestUriBuilder {
Copy link
Member

Choose a reason for hiding this comment

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

This API be combined with AuthorizationGrantTokenExchanger?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't feel we should combine these 2 API's.
AuthorizationRequestUriBuilder is used for authorization_code and implicit grant types whereas AuthorizationGrantTokenExchanger is used for authorization_code, client_credentials and resource_owner_password grant types.

We also have only one default implementation for AuthorizationRequestUriBuilder that will serve for authorization_code and implicit grant types. We will need different implementations of AuthorizationGrantTokenExchanger for each of authorization_code, client_credentials and resource_owner_password grant types.

/**
* @author Joe Grandja
*/
public abstract class AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
Copy link
Member

Choose a reason for hiding this comment

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

We don't need this abstract class. We can extract it later if necessary. For now move everything into AuthorizationCodeGrantAuthenticationToken

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See comment

@kakawait
Copy link

kakawait commented Mar 27, 2017

@jgrandja @rwinch Stupid question but is not actually the goal of Spring Social?

I mean with generic ApiAdapter that could work with any providers (like Oauth2ApiAdapter) and SpringSocialConfigurer.

@jgrandja
Copy link
Contributor Author

Hi @kakawait . As mentioned in #3907, OAuth-specific code is spread out in a few projects so we are looking to provide a uniform lower-level base that all projects can leverage. The goal is to have the other projects, for example Spring Social, leverage the lower-level protocol flow logic that will be provided within Spring Security.

@kakawait
Copy link

@jgrandja Great news, because I had the same issue when I wanted to add social login on my own UAA implementation, I found multiple way to achieve:

  1. Spring Social (http://docs.spring.io/spring-social/docs/1.1.4.RELEASE/reference/htmlsingle/#section_signin)
  2. Spring Security Oauth2 (https://spring.io/guides/tutorials/spring-boot-oauth2/)
  3. And other custom dirty way

I personally choose Spring Social because it supports OAuth 1 in addition to OAuth 2 (not because I'm fan of OAuth 1 but unfortunately Twitter still on OAuth 1) .

Thus I think I will not plan to support an uniform lower-level base for OAuth 1? I'm ok with that since OAuth 1 is dying.

@jgrandja
Copy link
Contributor Author

@rwinch All requested changes have been completed except for the following.

When I login using my GitHub client, I am able to see the email in the User Info page. I'm not sure why it's not showing up when you login with your client.

I still need to look at the STS/Buildship issue.

I'll work on the README for the sample next.

@jgrandja
Copy link
Contributor Author

@rwinch I think the URI naming convention you suggested makes sense.
Let's go with that for now.
The URI's have been changed as follows::

/oauth2/authorization/code/github for initiating the Authorization Code Request

/oauth2/authorize/code/github for handling the Authorization Code Response (which is essentially the registered redirect_uri)

@rwinch
Copy link
Member

rwinch commented Mar 28, 2017

  • OAuth is now first class so we shouldn't need to use apply(..). Add HttpSecurity.oauth2Login()
  • change URI to String across the board so that we can do things like variables in the URIs and client side load balancing

google:
client-id: your-app-client-id
client-secret: your-app-client-secret
redirect-uri: http://localhost:8080/oauth2/authorize/code/google
Copy link
Member

Choose a reason for hiding this comment

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

We should be able to default the redirect URI.

We know the host at the time the request comes in. We also know the client alias at this time (it is used to look up the details).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via 4d94134

}
}

private static class OAuth2ClientCondition extends SpringBootCondition implements ConfigurationCondition {
Copy link
Member

Choose a reason for hiding this comment

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

I think you can just remove this and it's subclasses and use @ConditionalOnProperty directly on the configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via 68f4891

public class OktaClientProperties extends ClientRegistrationProperties {
public static final String CLIENT_NAME = "Okta";
public static final String CLIENT_ALIAS = "okta";
public static final String AUTHORIZATION_URI = "https://your-subdomain.oktapreview.com/oauth2/v1/authorize";
Copy link
Member

Choose a reason for hiding this comment

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

Only default values that reduce the amount of configuration the user needs to provide. Defaults that are incorrect should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via 68f4891

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(ClientRegistration.class)
public class ClientRegistrationAutoConfiguration {
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to see this support creating a ClientRegistration from arbitrary client aliases rather than just specific aliases. The client aliases with a convention (i.e. google) simply provide defaults. The client aliases that are unknown the user should still only need to provide environment entries (no need to create the bean)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via 68f4891

* @author Joe Grandja
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
Copy link
Member

Choose a reason for hiding this comment

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

SecurityConfig should not be required. Instead we should allow the user to provide the java class and URL to map the user info in the environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via 8244855

@jgrandja
Copy link
Contributor Author

jgrandja commented Apr 3, 2017

@rwinch In reference to your comment...

email is displayed as empty for github client

It looks like your email address is set as private.
The GitHub documentation for the Get Single User endpoint states the following:

Note: The returned email is the user's publicly visible email address (or null if the user has not specified a public email address in their profile).

@jgrandja
Copy link
Contributor Author

jgrandja commented Apr 4, 2017

@rwinch I changed the member types from URI to String for most cases with the exception of OAuth2LoginConfigurer.UserInfoEndpointConfig.userInfoTypeMapping and MappableUserInfoType.
I think it makes sense to keep the type as URI in these cases.

/**
* @author Joe Grandja
*/
public final class ResponseAttributesExtractor {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this should be static classes. Instead perhaps consider implementing the Converter (or similar) API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via b876eda

* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core.protocol;
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this belongs in the core protocol package. There will be a reactive implementation of the OAuth support that does not support HttpServletRequest. Rather I think we should create a new package that contains converters used for the web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via b876eda

@rwinch
Copy link
Member

rwinch commented Apr 13, 2017

It would be good to also place the oauth2 stuff in a folder so all oauth2 stuff is within it.

private final String uri;
private final HttpStatus statusCode;

public enum ErrorCode {
Copy link
Member

Choose a reason for hiding this comment

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

I struggle with this being an Enum because I don't think we will ever be able to guarantee all the errors are enumerated (which is what an Enum is). We could potentially allow OAuth2Error to accept an ErrorCode interface and then create some constants for well known errors

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via 55f15f6

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {

if (this.isAuthorizationCodeErrorResponse(request)) {
Copy link
Member

Choose a reason for hiding this comment

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

Extract to leverage a converter API and if it converts to OAuth2Error then throw exception

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved via b876eda

@jgrandja jgrandja merged commit 829c386 into spring-projects:master Apr 28, 2017
@jgrandja jgrandja deleted the oauth2-support branch July 17, 2018 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants