Search code examples
flutterspring-securitysingle-sign-onamazon-cognito

SSO between App and webview inside the app


My user signs into my app using Amazon Cognito using this plugin.

I also have a spring boot application ui, secured by cognito as well.

At some point in my app flow, i want to show a webview of the spring boot application to let the user configure additional stuff.

How do i do it without having the user sign in again?

Would it be bad practice if i created an endpoint called /login/{username}/{password} that uses the SecurityContextHolder to sign the user in and redirect to /home?


Solution

  • I finally got it working.

    First i logged in, and made my code stop somewhere using the debugger, so i could look up the SecurityContextHolder.getContext().getAuthentication(). My Authentication object is of type OAuth2AuthenticationToken. I took a close look at it, and decided to replicate it. I did so inside a custom AuthenticationManager, and returned my OAuth2AuthenticationToken in the overriden authenticate method.

    CustomAuthenticationManager.java

    @Component
    public class CustomAuthenticationManager implements AuthenticationManager {
    
        @Bean
        protected PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String token = ((Jwt)authentication.getPrincipal()).getTokenValue();
            if (token == null)
                throw new BadCredentialsException("Invalid token");
            return convertAccessToken(token);
        }
    
        public OAuth2AuthenticationToken convertAccessToken(String accessToken){
            Jwt decode = Tools.parseToken(accessToken);
    
            List<GrantedAuthority> authorities = new ArrayList<>();
            for (String s : ((String[]) decode.getClaims().get("cognito:groups"))) {
                authorities.add(new SimpleGrantedAuthority("ROLE_" + s));
            }
            Map<String, Object> claims = decode.getClaims();
            OidcIdToken oidcIdToken = new OidcIdToken(decode.getTokenValue(), decode.getIssuedAt(), decode.getExpiresAt(), claims);
            DefaultOidcUser user = new DefaultOidcUser(authorities, oidcIdToken, "email");
            return new OAuth2AuthenticationToken(user, authorities, "cognito");
        }
    
    }
    

    Also i put this in a static Tools.java

        public static Jwt parseToken(String accessToken) {
            DecodedJWT decode = com.auth0.jwt.JWT.decode(accessToken);
            HashMap<String, Object> headers = new HashMap<>();
            headers.put("alg", decode.getHeaderClaim("alg").asString());
            headers.put("kid", decode.getHeaderClaim("kid").asString());
    
            HashMap<String, Object> claims = new HashMap<>();
            decode.getClaims().forEach((k, v) -> {
                switch(k){
                    case "cognito:roles":
                    case "cognito:groups":
                        claims.put(k, v.asArray(String.class));
                        break;
                    case "auth_time":
                    case "exp":
                    case "iat":
                        claims.put(k, v.asLong());
                        break;
                    default:
                        claims.put(k, v.asString());
                        break;
                }
            });
    
            return new Jwt(accessToken, decode.getIssuedAt().toInstant(), decode.getExpiresAt().toInstant(), headers,  claims);
        }
    

    Then i created two endpoints. One that is my "login page", and one that my filter goes to. So in my login page i take in an access token, store it in the sesion, then redirect to my other endpoint that pasess through the filter.

    TokenLoginController.java

    @Component
    @RestController
    public class TokenLoginController {
    
        @GetMapping(value="/login/token/{token}")
        @PermitAll
        public void setSession(@PathVariable("token") String token, HttpSession session, HttpServletResponse response) throws IOException {
            session.setAttribute("access_token", token);
            response.sendRedirect("/login/token");
        }
    
        @GetMapping(value="/login/token")
        @PermitAll
        public void setSession() {
    
        }
    
    }
    
    

    The filter extends AbstractAuthenticationProcessingFilter and looks up the access token from the session, creates the OAuth2AuthenticationToken, and authenticates with it.

    StickyAuthenticationFilter.java

    public class StickyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        public StickyAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
            super(defaultFilterProcessesUrl);
            setAuthenticationManager(authenticationManager);
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws AuthenticationException, IOException, ServletException {
    
            String access_token = (String)servletRequest.getSession().getAttribute("access_token");
            if (access_token != null) {
                JwtAuthenticationToken authRequest = new JwtAuthenticationToken(Tools.parseToken(access_token));
                return getAuthenticationManager().authenticate(authRequest);
            }
    
            throw new RuntimeException("Invalid access token");
        }
    
    }
    
    

    And finally, my SecurityConfig ties it all together like this:

    @EnableWebSecurity
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends VaadinWebSecurity {
    
        private final ClientRegistrationRepository clientRegistrationRepository;
    
        public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
            this.clientRegistrationRepository = clientRegistrationRepository;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().authorizeRequests().antMatchers("/login/token/*", "/login/token").permitAll().and()
                    .addFilterBefore(new StickyAuthenticationFilter("/login/token", new CustomAuthenticationManager()), BearerTokenAuthenticationFilter.class)
                    .oauth2ResourceServer(oauth2 -> oauth2.jwt())
                    .authorizeRequests()
                    .antMatchers("/user/**")
                    .authenticated();
            super.configure(http);
            setOAuth2LoginPage(http, "/oauth2/authorization/cognito");
            http.oauth2Login(l -> l.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper()));
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            // Customize your WebSecurity configuration.
            super.configure(web);
        }
    
        @Bean
        public GrantedAuthoritiesMapper userAuthoritiesMapper() {
            return (authorities) -> {
                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    
                Optional<OidcUserAuthority> awsAuthority = (Optional<OidcUserAuthority>) authorities.stream()
                        .filter(grantedAuthority -> "ROLE_USER".equals(grantedAuthority.getAuthority()))
                        .findFirst();
    
                if (awsAuthority.isPresent()) {
                    if (awsAuthority.get().getAttributes().get("cognito:groups") != null) {
                        mappedAuthorities = ((JSONArray) awsAuthority.get().getAttributes().get("cognito:groups")).stream()
                                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                                .collect(Collectors.toSet());
                    }
                }
    
                return mappedAuthorities;
            };
        }
    }