简体   繁体   中英

Spring Security OAuth 1.0 flow - Consumer verification

I have an external partner that uses OAuth 1.0 to protect some resources. I need to access this resources and I would like to do this using Spring Boot and Spring Security OAuth. As I don't want to use XML configuration, I already searched for a way to set up everything via Java configuration. I found this thread that provided an example of how to do this. But serveral things regarding the OAuth 1.0 flow are not clear for me.

My partner provides four endpoints for OAuth: an endpoint that provides a consumer token, a request_token endpoint, an authorization endpoint and an access_token endpoint. With my current setup (shown below) I can get a request token and the authorization endpoint gets called. However, the authorization endpoint does not ask for confirmation, but expects as URL parameters an email and a password and, after checking the credentials, returns the following:

oauth_verifier=a02ebdc5433242e2b6e582e17b84e313

And this is where the OAuth flow gets stuck.

After reading some articles about OAuth 1.0 the usual flow is this:

  1. get consumer token / key
  2. get oauth token using the consumer token via request_token endpoint
  3. redirect to authorization URL and ask the user for confirmation
  4. redirect to consumer with verifier token
  5. user verifier token and oauth token to get access token via access_token endpoint

First of all: steps 3 and 4 are not clear to me. I've found the Spring Security OAuth examples , but it wasn't clear to me how, after confirming the access, the user / verifier token get send back to the consumer. Could someone please explain how this is done?

Second: Given that my partners endpoint does not ask for confirmation but returns an oauth verifier right away, how can I use Spring Security OAuth with this setup? I was thinking about implementing my own authorization endpoint that calls the authorziation endpoint of my partner and then somehow makes the verifier known to my consumer, but I'm not sure how to do the latter part.

Here is the code so far (with help for the thread mentioned above; the ConsumerTokenDto has been left out as it is trivial):

Application

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Endpoint

@RestController
public class Endpoint {
    @Autowired
    private OAuthRestTemplate oAuthRestTemplate;
    private String url = "https://....";

    @RequestMapping("/public/v1/meters")
    public String getMeters() {
        try {
            return oAuthRestTemplate.getForObject(URI.create(url), String.class);
        } catch (Exception e) {
            LOG.error("Exception", e);
            return "";
        }
    }
}

OAuth configuration

@Configuration
@EnableWebSecurity
public class OAuthConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private RestTemplateBuilder restTemplateBuilder;
    private ConsumerTokenDto consumerTokenDto;

    private static final String ID = "meters";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").permitAll();
        http.addFilterAfter(this.oauthConsumerContextFilter(), SwitchUserFilter.class);
        http.addFilterAfter(this.oauthConsumerProcessingFilter(), OAuthConsumerContextFilterImpl.class);
    }

    private OAuthConsumerContextFilter oauthConsumerContextFilter() {
        OAuthConsumerContextFilter filter = new OAuthConsumerContextFilter();
        filter.setConsumerSupport(this.consumerSupport());
        return filter;
    }

    private OAuthConsumerProcessingFilter oauthConsumerProcessingFilter() {
        OAuthConsumerProcessingFilter filter = new OAuthConsumerProcessingFilter();
        filter.setProtectedResourceDetailsService(this.prds());

        LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> map = new LinkedHashMap<>();

        // one entry per oauth:url element in xml
        map.put(
                new AntPathRequestMatcher("/public/v1/**", null),
                Collections.singletonList(new SecurityConfig(ID)));

        filter.setObjectDefinitionSource(new DefaultFilterInvocationSecurityMetadataSource(map));

        return filter;
    }

    @Bean
    OAuthConsumerSupport consumerSupport() {
        CoreOAuthConsumerSupport consumerSupport = new CoreOAuthConsumerSupport();
        consumerSupport.setProtectedResourceDetailsService(prds());
        return consumerSupport;
    }

    @Bean
    ProtectedResourceDetailsService prds() {
        InMemoryProtectedResourceDetailsService service = new InMemoryProtectedResourceDetailsService();
        Map<String, ProtectedResourceDetails> store = new HashMap<>();
        store.put(ID, prd());
        service.setResourceDetailsStore(store);
        return service;
    }

    ProtectedResourceDetails prd() {
        ConsumerTokenDto consumerToken = getConsumerToken();
        BaseProtectedResourceDetails resourceDetails = new BaseProtectedResourceDetails();
        resourceDetails.setId(ID);
        resourceDetails.setConsumerKey(consumerToken.getKey());
        resourceDetails.setSharedSecret(new SharedConsumerSecretImpl(consumerToken.getSecret()));
        resourceDetails.setRequestTokenURL("https://.../request_token");
        // the authorization URL does not prompt for confirmation but immediately returns an OAuth verifier
        resourceDetails.setUserAuthorizationURL(
                "https://.../authorize?email=mail&password=pw");
        resourceDetails.setAccessTokenURL("https://.../access_token");
        resourceDetails.setSignatureMethod(HMAC_SHA1SignatureMethod.SIGNATURE_NAME);
        return resourceDetails;
    }

    // get consumer token from provider
    private ConsumerTokenDto getConsumerToken() {
        if (consumerTokenDto == null) {
            MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
            body.add("client", "Client");

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

            HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);

            RestTemplate restTemplate = restTemplateBuilder.setConnectTimeout(1000).setReadTimeout(1000).build();
            restTemplate.getInterceptors().add(interceptor);
            restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
            ResponseEntity<ConsumerTokenDto> response = restTemplate
                    .exchange("https://.../consumer_token", HttpMethod.POST, request,
                            ConsumerTokenDto.class);

            consumerTokenDto = response.getBody();
        }
        return consumerTokenDto;
    }

    // create oauth rest template
    @Bean
    public OAuthRestTemplate oAuthRestTemplate() {
        OAuthRestTemplate oAuthRestTemplate = new OAuthRestTemplate(prd());
        oAuthRestTemplate.getInterceptors().add(interceptor);
        return oAuthRestTemplate;
    }
}

I think I've found a solution. The trick is to implement my own OAuthConsumerContextFilter and replace the redirect call with a direct call to the authorization endpoint. I've commented the interesting parts below (starting with //!!!! ).

CustomOAuthConsumerContextFilter

public class CustomOAuthConsumerContextFilter extends OAuthConsumerContextFilter {

    private static final Logger LOG = LoggerFactory.getLogger(CustomOAuthConsumerContextFilter.class);

    private RestTemplateBuilder restTemplateBuilder;


    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        OAuthSecurityContextImpl context = new OAuthSecurityContextImpl();
        context.setDetails(request);

        Map<String, OAuthConsumerToken> rememberedTokens =
                getRememberMeServices().loadRememberedTokens(request, response);
        Map<String, OAuthConsumerToken> accessTokens = new TreeMap<>();
        Map<String, OAuthConsumerToken> requestTokens = new TreeMap<>();
        if (rememberedTokens != null) {
            for (Map.Entry<String, OAuthConsumerToken> tokenEntry : rememberedTokens.entrySet()) {
                OAuthConsumerToken token = tokenEntry.getValue();
                if (token != null) {
                    if (token.isAccessToken()) {
                        accessTokens.put(tokenEntry.getKey(), token);
                    } else {
                        requestTokens.put(tokenEntry.getKey(), token);
                    }
                }
            }
        }

        context.setAccessTokens(accessTokens);
        OAuthSecurityContextHolder.setContext(context);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Storing access tokens in request attribute '" + getAccessTokensRequestAttribute() + "'.");
        }

        try {
            try {
                request.setAttribute(getAccessTokensRequestAttribute(), new ArrayList<>(accessTokens.values()));
                chain.doFilter(request, response);
            } catch (Exception e) {
                try {
                    ProtectedResourceDetails resourceThatNeedsAuthorization = checkForResourceThatNeedsAuthorization(e);
                    String neededResourceId = resourceThatNeedsAuthorization.getId();
                    //!!!! store reference to verifier here, outside of loop
                    String verifier = null;
                    while (!accessTokens.containsKey(neededResourceId)) {
                        OAuthConsumerToken token = requestTokens.remove(neededResourceId);
                        if (token == null) {
                            token = getTokenServices().getToken(neededResourceId);
                        }

                        // if the token is null OR
                        // if there is NO access token and (we're not using 1.0a or the verifier is not null)
                        if (token == null || (!token.isAccessToken() &&
                                (!resourceThatNeedsAuthorization.isUse10a() || verifier == null))) {
                            //no token associated with the resource, start the oauth flow.
                            //if there's a request token, but no verifier, we'll assume that a previous oauth request failed and we need to get a new request token.
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Obtaining request token for resource: " + neededResourceId);
                            }

                            //obtain authorization.
                            String callbackURL = response.encodeRedirectURL(getCallbackURL(request));
                            token = getConsumerSupport().getUnauthorizedRequestToken(neededResourceId, callbackURL);
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Request token obtained for resource " + neededResourceId + ": " + token);
                            }

                            //okay, we've got a request token, now we need to authorize it.
                            requestTokens.put(neededResourceId, token);
                            getTokenServices().storeToken(neededResourceId, token);
                            String redirect =
                                    getUserAuthorizationRedirectURL(resourceThatNeedsAuthorization, token, callbackURL);

                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Redirecting request to " + redirect +
                                        " for user authorization of the request token for resource " +
                                        neededResourceId + ".");
                            }

                            request.setAttribute(
                                    "org.springframework.security.oauth.consumer.AccessTokenRequiredException", e);
                            //                            this.redirectStrategy.sendRedirect(request, response, redirect);
                            //!!!! get the verifier from the authorization URL
                            verifier = this.getVerifier(redirect);
                            //!!!! start next iteration of loop -> now we have the verifier, so the else statement below shoud get executed and an access token retrieved
                            continue;
                        } else if (!token.isAccessToken()) {
                            //we have a presumably authorized request token, let's try to get an access token with it.
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Obtaining access token for resource: " + neededResourceId);
                            }

                            //authorize the request token and store it.
                            try {
                                token = getConsumerSupport().getAccessToken(token, verifier);
                            } finally {
                                getTokenServices().removeToken(neededResourceId);
                            }

                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Access token " + token + " obtained for resource " + neededResourceId +
                                        ". Now storing and using.");
                            }

                            getTokenServices().storeToken(neededResourceId, token);
                        }

                        accessTokens.put(neededResourceId, token);

                        try {
                            //try again
                            if (!response.isCommitted()) {
                                request.setAttribute(getAccessTokensRequestAttribute(),
                                        new ArrayList<>(accessTokens.values()));
                                chain.doFilter(request, response);
                            } else {
                                //dang. what do we do now?
                                throw new IllegalStateException(
                                        "Unable to reprocess filter chain with needed OAuth2 resources because the response is already committed.");
                            }
                        } catch (Exception e1) {
                            resourceThatNeedsAuthorization = checkForResourceThatNeedsAuthorization(e1);
                            neededResourceId = resourceThatNeedsAuthorization.getId();
                        }
                    }
                } catch (OAuthRequestFailedException eo) {
                    fail(request, response, eo);
                } catch (Exception ex) {
                    Throwable[] causeChain = getThrowableAnalyzer().determineCauseChain(ex);
                    OAuthRequestFailedException rfe = (OAuthRequestFailedException) getThrowableAnalyzer()
                            .getFirstThrowableOfType(OAuthRequestFailedException.class, causeChain);
                    if (rfe != null) {
                        fail(request, response, rfe);
                    } else {
                        // Rethrow ServletExceptions and RuntimeExceptions as-is
                        if (ex instanceof ServletException) {
                            throw (ServletException) ex;
                        } else if (ex instanceof RuntimeException) {
                            throw (RuntimeException) ex;
                        }

                        // Wrap other Exceptions. These are not expected to happen
                        throw new RuntimeException(ex);
                    }
                }
            }
        } finally {
            OAuthSecurityContextHolder.setContext(null);
            HashMap<String, OAuthConsumerToken> tokensToRemember = new HashMap<>();
            tokensToRemember.putAll(requestTokens);
            tokensToRemember.putAll(accessTokens);
            getRememberMeServices().rememberTokens(tokensToRemember, request, response);
        }
    }


    private String getVerifier(String authorizationURL) {
        HttpEntity request = HttpEntity.EMPTY;

        RestTemplate restTemplate = restTemplateBuilder.setConnectTimeout(1000).setReadTimeout(1000).build();
        ResponseEntity<String> response =
                restTemplate.exchange(authorizationURL, HttpMethod.GET, request, String.class);
        //!!!! extract verifier from response
        String verifier = response.getBody().split("=")[1];
        return verifier;
    }


    void setRestTemplateBuilder(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplateBuilder = restTemplateBuilder;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM