Skip to content

[Enhancement] Allow deep customize for nonstandard OAuth 2.0 provider. #17148

@zhangyanwei

Description

@zhangyanwei

Recently, I have tried to make my application as wechat OAuth 2.0 client like GitHub, Google, etc.
But the wechat not strict follows the specification, so the following configuration does not work.

spring:
    oauth2.client:
      provider:
        wechat:
          authorization-uri: https://open.weixin.qq.com/connect/oauth2/authorize
          token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
          user-info-authentication-method: query
          user-name-attribute: nickname

That because wechat requires the parameters' order of authorization request URL strictly follows their requirement (for security reason even if there is no any helps.)
To make it works, I have to customize the URL building in org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest$Builder, but there is no convenient and graceful way to do it.

As a workaround, I added additional properties on the provider, then using javassist to instrument buildAuthorizationRequestUri method.

A sample of modified properties.

spring:
  security:
    oauth2.client:
      provider:
        wechat:
          authorization-uri: https://open.weixin.qq.com/connect/oauth2/authorize
          token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
          user-info-authentication-method: query
          user-name-attribute: nickname
          nonstandard:
            client-id-parameter: appid
            ordered-parameters: appid,redirect_uri,response_type,scope,state
            fragment: wechat_redirect

A javassist sample for how to instrument the private method.

/**
 * @see OAuth2AuthorizationInstrumentRunListener#instrumentOAuth2AuthorizationRequestBuilder()
 */
public class OAuth2AuthorizationRequestBuilder {

    private String authorizationUri;
    private AuthorizationGrantType authorizationGrantType;
    private OAuth2AuthorizationResponseType responseType;
    private String clientId;
    private String redirectUri;
    private Set<String> scopes;
    private String state;
    private Map<String, Object> additionalParameters;
    private String authorizationRequestUri;

    public String buildAuthorizationRequestUri() {

        NonstandardOAuth2ClientProperties nonstandardProperties = NonstandardOAuth2ClientProperties.getInstance();
        checkState(nonstandardProperties != null, "NonstandardOAuth2ClientProperties not initialized.");

        String registrationId = (String) this.additionalParameters.get(OAuth2ParameterNames.REGISTRATION_ID);
        checkState(!isNullOrEmpty(registrationId), "Can not detect the OAuth 2.0 client registration ID.");

        NonstandardOAuth2ClientProperties.NonstandardProvider nonstandardProvider = nonstandardProperties.getProvider(registrationId);

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
        parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue());

        String clientIdParameterName = isNullOrEmpty(nonstandardProvider.getClientIdParameter()) ?
                OAuth2ParameterNames.CLIENT_ID : nonstandardProvider.getClientIdParameter();
        parameters.set(clientIdParameterName, this.clientId);

        if (!CollectionUtils.isEmpty(this.scopes)) {
            parameters.set(OAuth2ParameterNames.SCOPE,
                    StringUtils.collectionToDelimitedString(this.scopes, " "));
        }

        if (this.state != null) {
            parameters.set(OAuth2ParameterNames.STATE, this.state);
        }

        if (this.redirectUri != null) {
            parameters.set(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri);
        }

        if (!CollectionUtils.isEmpty(this.additionalParameters)) {
            // Do not using the lambda expression because of the javassist bug.
            // https://github.com/jboss-javassist/javassist/issues/262
            Set<Map.Entry<String, Object>> entries = this.additionalParameters.entrySet();
            for (Map.Entry<String, Object> entry : entries) {
                String key = entry.getKey();
                if (!key.equals(OAuth2ParameterNames.REGISTRATION_ID)) {
                    parameters.set(key, entry.getValue().toString());
                }
            }
        }

        // Re-order the parameters
        List<String> orderedKeys = nonstandardProvider.getOrdered();
        Set<String> parameterKeys = new LinkedHashSet<>(parameters.size());
        parameterKeys.addAll(orderedKeys);
        parameterKeys.addAll(parameters.keySet());
        MultiValueMap<String, String> orderedParameters = new LinkedMultiValueMap<>();
        for (String parameterKey : parameterKeys) {
            orderedParameters.set(parameterKey, parameters.getFirst(parameterKey));
        }
        return UriComponentsBuilder.fromHttpUrl(this.authorizationUri)
                .queryParams(orderedParameters)
                .fragment(nonstandardProvider.getFragment())
                .encode(StandardCharsets.UTF_8)
                .build()
                .toUriString();
    }
}
// http://www.labouisse.com/quicky/2015/09/23/javassisting-spring-boot
public void instrumentOAuth2AuthorizationRequestBuilder() {
    try {
        String classname = "org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest$Builder";
        String replacerClassname = "com.github.zhangyanwei.wechat.wcms.instrument.replacer.OAuth2AuthorizationRequestBuilder";
        String methodName = "buildAuthorizationRequestUri";
        ClassPool cp = ClassPool.getDefault();
        cp.appendClassPath(new LoaderClassPath(application.getClassLoader()));

        // Find the target method.
        CtClass cc = cp.get(classname);
        CtMethod cm = cc.getDeclaredMethod(methodName);

        // Replace with the template method.
        CtClass rcc = cp.get(replacerClassname);
        CtMethod rcm = rcc.getDeclaredMethod(methodName);
        cm.setBody(rcm, null);

        cc.toClass();
        cc.detach();
        rcc.detach();
    } catch (NotFoundException | CannotCompileException e) {
        throw new RuntimeException(e);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    for: external-projectFor an external project and not something we can fixstatus: invalidAn issue that we don't feel is valid

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions