Danushka
Danushka

Reputation: 67

Spring Boot + Oauth 2 Single Sign-On ridirect uri misbehaviour

I need to implement Single Sign-On application using Spring Boot (2.1.9.RELEASE) and OAuth2. I have created two client application and authentication server as well. When i hit client application URL it successfully redirect to the authentication server and validate username and password. But when it redirect back to the client application always gives below oauth error.

error="invalid_grant", error_description="Invalid redirect: http://localhost:8082/app1/login does not match one of the registered values: [http://localhost:8082/app1]"

Here i have noticed that always append /login path automatically to the end of redirect uri. Maybe it is the default behavior of the Spring Boot SSO. I have tried many ways to resolve this error but couldn't. Can anyone help me.

I have followed sample project sample project

Steps

  1. Hit client application url (http://localhost:8082/app1)
  2. Successfully redirect to the authentication server (http://localhost:8081/auth/login)
  3. enter username and password and validate
  4. redirect URL with error http://localhost:8081/auth/oauth/authorize?client_id=foo&redirect_uri=http://localhost:8082/app1/login&response_type=code&state=03W7yX

Authentication Server

POM file

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
        <spring-cloud.version>Finchley.SR1</spring-cloud.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>jakarta.xml.bind</groupId>
            <artifactId>jakarta.xml.bind-api</artifactId>
            <version>2.3.2</version>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.0-b170127.1453</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Main Class

@SpringBootApplication
@EnableResourceServer
public class TestProjectApplication {

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

Security config class

@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers("/login", "/oauth/authorize")
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("john")
                .password(passwordEncoder().encode("123"))
                .roles("USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Client Application

POM File

<properties>
        <java.version>11</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-cloud.version>Finchley.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Main Class

@SpringBootApplication
@EnableOAuth2Sso
public class App1Application implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }

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

}

Application.yml

server:
  port: 8082
  servlet:
    context-path: /app1
spring:
  main:
    allow-bean-definition-overriding: true
security:
  basic:
    enabled: false
  oauth2:
    client:
      clientId: foo
      clientSecret: bar
      accessTokenUri: http://localhost:8081/auth/oauth/token
      userAuthorizationUri: http://localhost:8081/auth/oauth/authorize
    resource:
      userInfoUri: http://localhost:8080/auth/user/me

Upvotes: 1

Views: 1343

Answers (3)

Bilal Demir
Bilal Demir

Reputation: 686

You can use Custom RedirectResolver.

Sample Code:

public class CustomRedirectResolver implements RedirectResolver {

    private Collection<String> redirectGrantTypes = Arrays.asList("implicit", "authorization_code");

    private boolean matchSubdomains = false;

    private boolean matchPorts = true;
    
    public void setMatchSubdomains(boolean matchSubdomains) {
        this.matchSubdomains = matchSubdomains;
    }
    
    public void setMatchPorts(boolean matchPorts) {
        this.matchPorts = matchPorts;
    }
    
    public void setRedirectGrantTypes(Collection<String> redirectGrantTypes) {
        this.redirectGrantTypes = new HashSet<String>(redirectGrantTypes);
    }

    public String resolveRedirect(String requestedRedirect, ClientDetails client) throws OAuth2Exception {

        Set<String> authorizedGrantTypes = client.getAuthorizedGrantTypes();
        if (authorizedGrantTypes.isEmpty()) {
            throw new InvalidGrantException("A client must have at least one authorized grant type.");
        }
        if (!containsRedirectGrantType(authorizedGrantTypes)) {
            throw new InvalidGrantException(
                    "A redirect_uri can only be used by implicit or authorization_code grant types.");
        }

        Set<String> registeredRedirectUris = client.getRegisteredRedirectUri();
        if (registeredRedirectUris == null || registeredRedirectUris.isEmpty()) {
            throw new InvalidRequestException("At least one redirect_uri must be registered with the client.");
        }
        return obtainMatchingRedirect(registeredRedirectUris, requestedRedirect);
    }
    
    private boolean containsRedirectGrantType(Set<String> grantTypes) {
        for (String type : grantTypes) {
            if (redirectGrantTypes.contains(type)) {
                return true;
            }
        }
        return false;
    }
    
    protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
        UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
        UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();

        boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
        boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
        boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
        boolean portMatch = !matchPorts || registeredRedirectUri.getPort() == requestedRedirectUri.getPort();

        // path match condition removed
        return schemeMatch && userInfoMatch && hostMatch && portMatch;
    }
    
    private boolean matchQueryParams(MultiValueMap<String, String> registeredRedirectUriQueryParams,
                                     MultiValueMap<String, String> requestedRedirectUriQueryParams) {


        Iterator<String> iter = registeredRedirectUriQueryParams.keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            List<String> registeredRedirectUriQueryParamsValues = registeredRedirectUriQueryParams.get(key);
            List<String> requestedRedirectUriQueryParamsValues = requestedRedirectUriQueryParams.get(key);

            if (!registeredRedirectUriQueryParamsValues.equals(requestedRedirectUriQueryParamsValues)) {
                return false;
            }
        }

        return true;
    }
    
    private boolean isEqual(String str1, String str2) {
        if (StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) {
            return true;
        } else if (!StringUtils.isEmpty(str1)) {
            return str1.equals(str2);
        } else {
            return false;
        }
    }
    
    protected boolean hostMatches(String registered, String requested) {
        if (matchSubdomains) {
            return isEqual(registered, requested) || (requested != null && requested.endsWith("." + registered));
        }
        return isEqual(registered, requested);
    }
    
    private String obtainMatchingRedirect(Set<String> redirectUris, String requestedRedirect) {
        Assert.notEmpty(redirectUris, "Redirect URIs cannot be empty");

        if (redirectUris.size() == 1 && requestedRedirect == null) {
            return redirectUris.iterator().next();
        }

        for (String redirectUri : redirectUris) {
            if (requestedRedirect != null && redirectMatches(requestedRedirect, redirectUri)) {
                // Initialize with the registered redirect-uri
                UriComponentsBuilder redirectUriBuilder = UriComponentsBuilder.fromUriString(redirectUri);

                UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();

                if (this.matchSubdomains) {
                    redirectUriBuilder.host(requestedRedirectUri.getHost());
                }
                if (!this.matchPorts) {
                    redirectUriBuilder.port(requestedRedirectUri.getPort());
                }
                redirectUriBuilder.replaceQuery(requestedRedirectUri.getQuery());       // retain additional params (if any)
                redirectUriBuilder.fragment(null);
                // RedirectUri changed to RequestRedirectUri
                return requestedRedirectUri.toUriString();
            }
        }

        throw new RedirectMismatchException("Invalid redirect: " + requestedRedirect
                + " does not match one of the registered values.");
    }
}

You need to configuration class:

@Configuration
public class AuthorizartionConfigurer extends AuthorizationServerConfigurerAdapter {

        
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // some configurations code here
        // ...
        endpoints.redirectResolver(new CustomRedirectResolver());
    }

}

Upvotes: 0

Neetu
Neetu

Reputation: 121

Faced the similar issue :

error="invalid_grant", error_description="Invalid redirect: http://localhost:8082/login does not match one of the registered values: [http://localhost:8082/]"

Updating below line from

client.redirectUris("http://localhost:9292")

to

client.redirectUris("http://localhost:9292/login");

resolved the issue for me.

Upvotes: 0

Danushka
Danushka

Reputation: 67

Finally can be found the answer for this. Issue is spring boot version. I have changed my spring boot version to 2.1.3.RELEASE. now it is working as expected. But still don't have idea why it is not working in latest spring boot version.

Upvotes: 1

Related Questions