Skip to content

Commit

Permalink
Refactors out custom UserInfoTokenServices to check and enforce restr…
Browse files Browse the repository at this point in the history
…icted domain param (#250)
  • Loading branch information
Travis Tomsu committed Jun 27, 2016
1 parent fc8f7eb commit afcb3d2
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 70 deletions.
2 changes: 1 addition & 1 deletion gate-web/config/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ spring:
# clientId:
# clientSecret:
accessTokenUri: https://www.googleapis.com/oauth2/v4/token
userAuthorizationUri: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline
userAuthorizationUri: https://accounts.google.com/o/oauth2/v2/auth
scope: "profile email"
resource:
userInfoUri: https://www.googleapis.com/oauth2/v3/userinfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,9 @@ package com.netflix.spinnaker.gate.security.oauth2

import com.netflix.spinnaker.gate.security.AuthConfig
import com.netflix.spinnaker.gate.security.SpinnakerAuthConfig
import com.netflix.spinnaker.gate.security.rolesprovider.UserRolesProvider
import com.netflix.spinnaker.gate.services.CredentialsService
import com.netflix.spinnaker.security.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.cloud.security.oauth2.resource.ResourceServerProperties
import org.springframework.cloud.security.oauth2.resource.UserInfoTokenServices
import org.springframework.cloud.security.oauth2.sso.EnableOAuth2Sso
import org.springframework.cloud.security.oauth2.sso.OAuth2SsoConfigurer
import org.springframework.cloud.security.oauth2.sso.OAuth2SsoConfigurerAdapter
Expand All @@ -36,13 +30,7 @@ import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Primary
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity
import org.springframework.security.core.AuthenticationException
import org.springframework.security.oauth2.common.OAuth2AccessToken
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException
import org.springframework.security.oauth2.provider.OAuth2Authentication
import org.springframework.security.oauth2.provider.OAuth2Request
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.stereotype.Component

/**
Expand Down Expand Up @@ -77,65 +65,10 @@ class OAuth2SsoConfig extends OAuth2SsoConfigurerAdapter {
AuthConfig.configure(http)
}

/**
* ResourceServerTokenServices is an interface used to manage access tokens. The UserInfoTokenService object is an
* implementation of that interface that uses an access token to get the logged in user's data (such as email or
* profile). We want to customize the Authentication object that is returned to include our custom (Kork) User.
*/
@Primary
@Bean
ResourceServerTokenServices spinnakerAuthorityInjectedUserInfoTokenServices() {
return new ResourceServerTokenServices() {

@Autowired
ResourceServerProperties sso

@Autowired
UserInfoTokenServices userInfoTokenServices

@Autowired
CredentialsService credentialsService

@Autowired
UserRolesProvider userRolesProvider

@Autowired
UserInfoMapping userInfoMapping

@Override
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
OAuth2Authentication oAuth2Authentication = userInfoTokenServices.loadAuthentication(accessToken)

Map details = oAuth2Authentication.userAuthentication.details as Map
def username = details[userInfoMapping.username] as String
def roles = userRolesProvider.loadRoles(username)

User spinnakerUser = new User(
email: details[userInfoMapping.email] as String,
firstName: details[userInfoMapping.firstName] as String,
lastName: details[userInfoMapping.lastName] as String,
allowedAccounts: credentialsService.getAccountNames(roles),
roles: roles,
username: username).asImmutable()

PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(
spinnakerUser,
null /* credentials */,
spinnakerUser.authorities
)

// impl copied from userInfoTokenServices
OAuth2Request storedRequest = new OAuth2Request(null, sso.clientId, null, true /*approved*/,
null, null, null, null, null);

return new OAuth2Authentication(storedRequest, authentication)
}

@Override
OAuth2AccessToken readAccessToken(String accessToken) {
return userInfoTokenServices.readAccessToken(accessToken)
}
}
ResourceServerTokenServices spinnakerUserInfoTokenServices() {
new SpinnakerUserInfoTokenServices()
}

/**
Expand All @@ -149,4 +82,9 @@ class OAuth2SsoConfig extends OAuth2SsoConfigurerAdapter {
String lastName = "family_name"
String username = "email"
}

@Component
@ConfigurationProperties("spring.oauth2.userInfoRequirements")
static class UserInfoRequirements extends HashMap<String, String> {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2016 Google, Inc.
*
* 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
*
* http://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 com.netflix.spinnaker.gate.security.oauth2

import com.netflix.spinnaker.gate.security.rolesprovider.UserRolesProvider
import com.netflix.spinnaker.gate.services.CredentialsService
import com.netflix.spinnaker.security.User
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cloud.security.oauth2.resource.ResourceServerProperties
import org.springframework.cloud.security.oauth2.resource.UserInfoTokenServices
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.AuthenticationException
import org.springframework.security.oauth2.common.OAuth2AccessToken
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException
import org.springframework.security.oauth2.provider.OAuth2Authentication
import org.springframework.security.oauth2.provider.OAuth2Request
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken

/**
* ResourceServerTokenServices is an interface used to manage access tokens. The UserInfoTokenService object is an
* implementation of that interface that uses an access token to get the logged in user's data (such as email or
* profile). We want to customize the Authentication object that is returned to include our custom (Kork) User.
*/
@Slf4j
class SpinnakerUserInfoTokenServices implements ResourceServerTokenServices {
@Autowired
ResourceServerProperties sso

@Autowired
UserInfoTokenServices userInfoTokenServices

@Autowired
CredentialsService credentialsService

@Autowired
UserRolesProvider userRolesProvider

@Autowired
OAuth2SsoConfig.UserInfoMapping userInfoMapping

@Autowired
OAuth2SsoConfig.UserInfoRequirements userInfoRequirements

@Override
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
OAuth2Authentication oAuth2Authentication = userInfoTokenServices.loadAuthentication(accessToken)

Map details = oAuth2Authentication.userAuthentication.details as Map

if (!hasAllUserInfoRequirements(details)) {
throw new BadCredentialsException("User's info does not have all required fields.")
}

def username = details[userInfoMapping.username] as String
def roles = userRolesProvider.loadRoles(username)

User spinnakerUser = new User(
email: details[userInfoMapping.email] as String,
firstName: details[userInfoMapping.firstName] as String,
lastName: details[userInfoMapping.lastName] as String,
allowedAccounts: credentialsService.getAccountNames(roles),
roles: roles,
username: username).asImmutable()

PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(
spinnakerUser,
null /* credentials */,
spinnakerUser.authorities
)

// impl copied from UserInfoTokenServices
OAuth2Request storedRequest = new OAuth2Request(null, sso.clientId, null, true /*approved*/,
null, null, null, null, null);

return new OAuth2Authentication(storedRequest, authentication)
}

@Override
OAuth2AccessToken readAccessToken(String accessToken) {
return userInfoTokenServices.readAccessToken(accessToken)
}

boolean hasAllUserInfoRequirements(Map details) {
if (!userInfoRequirements) {
return true
}

def invalidFields = userInfoRequirements.findAll { String reqKey, String reqVal ->
details[reqKey] != reqVal
}
if (log.debugEnabled) {
log.debug "Invalid userInfo response: " + invalidFields.collect({k, v -> "got $k=${details[k]}, wanted $v"}).join(", ")
}

return !invalidFields
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2016 Google, Inc.
*
* 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
*
* http://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 com.netflix.spinnaker.gate.security.oauth2

import spock.lang.Specification
import spock.lang.Subject

class SpinnakerUserInfoTokenServicesSpec extends Specification {

def "should check restricted domain flag if set"() {
setup:
def userInfoRequirements = new OAuth2SsoConfig.UserInfoRequirements();
@Subject tokenServices = new SpinnakerUserInfoTokenServices(userInfoRequirements: userInfoRequirements)

expect: "no domain restriction means everything matches"
tokenServices.hasAllUserInfoRequirements([:])
tokenServices.hasAllUserInfoRequirements(["hd": "foo.com"])
tokenServices.hasAllUserInfoRequirements(["bar": "foo.com"])
tokenServices.hasAllUserInfoRequirements(["bar": "bar.com"])

when: "domain restricted but not found on userAuthorizationUri"
userInfoRequirements.hd = "foo.com"

then:
!tokenServices.hasAllUserInfoRequirements([:])
tokenServices.hasAllUserInfoRequirements(["hd": "foo.com"])
!tokenServices.hasAllUserInfoRequirements(["bar": "foo.com"])
!tokenServices.hasAllUserInfoRequirements(["bar": "bar.com"])

when: "multiple restriction values"
userInfoRequirements.bar = "bar.com"

then:
!tokenServices.hasAllUserInfoRequirements(["hd": "foo.com"])
!tokenServices.hasAllUserInfoRequirements(["bar": "bar.com"])
tokenServices.hasAllUserInfoRequirements(["hd": "foo.com", "bar": "bar.com"])
}
}

0 comments on commit afcb3d2

Please sign in to comment.