Spring Security - Amostra de serviço REST com autenticação OAuth2 via BitBucket e JWT

No artigo anterior , desenvolvemos um aplicativo da web seguro simples que usava OAuth2 para autenticar usuários com o Bitbucket como servidor de autorização. Para alguns, tal bundle pode parecer estranho, mas imagine que estamos desenvolvendo um servidor CI (Continuous Integration) e gostaríamos de ter acesso aos recursos do usuário no sistema de controle de versão. Por exemplo, a conhecida plataforma de CI drone.io funciona com o mesmo princípio .



No exemplo anterior, uma sessão HTTP (e cookies) foi usada para autorizar solicitações ao servidor. Porém, para a implementação de um serviço REST, este método de autorização não é adequado, já que um dos requisitos da arquitetura REST é a falta de estado. Neste artigo implementaremos um serviço REST, cuja autorização de requisições será realizada através de um token de acesso.



Um pouco de teoria



A autenticação é o processo de verificação das credenciais do usuário (login / senha). A autenticação do usuário é realizada comparando o login / senha por ele inserido com os dados salvos.



A autorização é a verificação dos direitos de um usuário de acessar determinados recursos. A autorização é realizada diretamente quando o usuário acessa o recurso.



Vamos considerar a ordem de trabalho dos dois métodos de autorização de solicitações mencionados acima.

Autorizando solicitações usando uma sessão HTTP:



  • O usuário é autenticado de qualquer uma das maneiras.
  • Uma sessão HTTP é criada no servidor e um cookie JSESSIONID armazenando o identificador da sessão.
  • O cookie JSESSIONID é transmitido ao cliente e armazenado no navegador.
  • Com cada solicitação subsequente, um cookie JSESSIONID é enviado ao servidor.
  • O servidor encontra a sessão HTTP correspondente com informações sobre o usuário atual e determina se o usuário tem permissão para fazer essa chamada.
  • Para sair do aplicativo, você deve excluir a sessão HTTP do servidor.


Autorizando solicitações usando um token de acesso:



  • O usuário é autenticado de qualquer uma das maneiras.
  • O servidor gera um token de acesso assinado com uma chave privada e o envia ao cliente. O token contém o identificador do usuário e sua função.
  • O token é armazenado no cliente e transmitido ao servidor com cada solicitação subsequente. Normalmente, o cabeçalho HTTP Authorization é usado para transferir o token.
  • O servidor verifica a assinatura do token, extrai dele o ID do usuário, sua função e determina se o usuário tem direitos para fazer essa chamada.
  • Para sair do aplicativo, basta remover o token do cliente sem precisar interagir com o servidor.


O JSON Web Token (JWT) é atualmente um formato de token de acesso comum. Um token JWT contém três blocos, separados por pontos: cabeçalho, carga útil e assinatura. Os primeiros dois blocos estão no formato JSON e codificados no formato base64. O conjunto de campos pode consistir em nomes reservados (iss, iat, exp) ou pares arbitrários de nome / valor. A assinatura pode ser gerada usando algoritmos de criptografia simétricos e assimétricos.



Implementação



Implementaremos um serviço REST que fornece a seguinte API:



  • GET / auth / login - inicia o processo de autenticação do usuário.
  • POST / auth / token - solicita um novo par de tokens de acesso / atualização.
  • GET / api / repositories - Obtenha a lista de repositórios Bitbucket do usuário atual.




Arquitetura de aplicativo de alto nível



Observe que, como o aplicativo consiste em três componentes de interação, além de autorizar solicitações do cliente para o servidor, o Bitbucket autoriza solicitações do servidor para ele. Não configuraremos a autorização de método por função, para não complicar o exemplo. Temos apenas um método API GET / api / repositories que só pode ser chamado por usuários autenticados. O servidor pode executar qualquer operação no Bitbucket permitida pelo registro OAuth do cliente.





O processo de registro do cliente OAuth é descrito no artigo anterior .



Para implementação, usaremos Spring Boot versão 2.2.2.RELEASE e Spring Security versão 5.2.1.RELEASE.



Substituir AuthenticationEntryPoint



Em um aplicativo da web padrão, quando um recurso seguro é acessado e não há nenhum objeto de autenticação no contexto de segurança, Spring Security redirecionará o usuário para a página de autenticação. No entanto, para um serviço REST, o comportamento mais apropriado nesse caso seria retornar o status HTTP 401 (NÃO AUTORIZADO).



RestAuthenticationEntryPoint
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}




Crie um endpoint de login



Ainda estamos usando OAuth2 com o tipo de autorização do Código de Autorização para autenticação do usuário. No entanto, na etapa anterior, substituímos o AuthenticationEntryPoint padrão por nossa própria implementação, portanto, precisamos de uma maneira explícita de iniciar o processo de autenticação. Quando enviamos uma solicitação GET para / auth / login, redirecionamos o usuário para a página de autenticação do Bitbucket. O parâmetro deste método será a URL de retorno de chamada, onde retornaremos o token de acesso após a autenticação bem-sucedida.



Endpoint de login
@Path("/auth")
public class AuthEndpoint extends EndpointBase {

...

    @GET
    @Path("/login")
    public Response authorize(@QueryParam(REDIRECT_URI) String redirectUri) {
        String authUri = "/oauth2/authorization/bitbucket";
        UriComponentsBuilder builder = fromPath(authUri).queryParam(REDIRECT_URI, redirectUri);
        return handle(() -> temporaryRedirect(builder.build().toUri()).build());
    }
}




Substituir AuthenticationSuccessHandler



AuthenticationSuccessHandler é chamado após a autenticação bem-sucedida. Vamos gerar um token de acesso aqui, um token de atualização e redirecionar para o endereço de retorno de chamada que foi enviado no início do processo de autenticação. Retornamos o token de acesso com o parâmetro de solicitação GET e o token de atualização no cookie httpOnly. Analisaremos o que é um token de atualização mais tarde.



ExampleAuthenticationSuccessHandler
public class ExampleAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final TokenService tokenService;

    private final AuthProperties authProperties;

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    public ExampleAuthenticationSuccessHandler(
            TokenService tokenService,
            AuthProperties authProperties,
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.tokenService = requireNonNull(tokenService);
        this.authProperties = requireNonNull(authProperties);
        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("Logged in user {}", authentication.getPrincipal());
        super.onAuthenticationSuccess(request, response, authentication);
    }

    @Override
    protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        Optional<String> redirectUri = getCookie(request, REDIRECT_URI).map(Cookie::getValue);

        if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
            throw new BadRequestException("Received unauthorized redirect URI.");
        }

        return UriComponentsBuilder.fromUriString(redirectUri.orElse(getDefaultTargetUrl()))
                .queryParam("token", tokenService.newAccessToken(toUserContext(authentication)))
                .build().toUriString();
    }

    @Override
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        redirectToTargetUrl(request, response, authentication);
    }

    private boolean isAuthorizedRedirectUri(String uri) {
        URI clientRedirectUri = URI.create(uri);
        return authProperties.getAuthorizedRedirectUris()
                .stream()
                .anyMatch(authorizedRedirectUri -> {
                    // Only validate host and port. Let the clients use different paths if they want to.
                    URI authorizedURI = URI.create(authorizedRedirectUri);
                    return authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
                            && authorizedURI.getPort() == clientRedirectUri.getPort();
                });
    }

    private TokenService.UserContext toUserContext(Authentication authentication) {
        ExampleOAuth2User principal = (ExampleOAuth2User) authentication.getPrincipal();
        return TokenService.UserContext.builder()
                .login(principal.getName())
                .name(principal.getFullName())
                .build();
    }

    private void addRefreshTokenCookie(HttpServletResponse response, Authentication authentication) {
        RefreshToken token = tokenService.newRefreshToken(toUserContext(authentication));
        addCookie(response, REFRESH_TOKEN, token.getId(), (int) token.getValiditySeconds());
    }

    private void redirectToTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        String targetUrl = determineTargetUrl(request, response, authentication);

        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
            return;
        }

        addRefreshTokenCookie(response, authentication);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}




Substituir AuthenticationFailureHandler



Caso o usuário não esteja autenticado, iremos redirecioná-lo para o endereço de retorno que foi passado no início do processo de autenticação com o parâmetro error contendo o texto do erro.



ExampleAuthenticationFailureHandler
public class ExampleAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    public ExampleAuthenticationFailureHandler(
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        String targetUrl = getFailureUrl(request, exception);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    private String getFailureUrl(HttpServletRequest request, AuthenticationException exception) {
        String targetUrl = getCookie(request, Cookies.REDIRECT_URI)
                .map(Cookie::getValue)
                .orElse(("/"));

        return UriComponentsBuilder.fromUriString(targetUrl)
                .queryParam("error", exception.getLocalizedMessage())
                .build().toUriString();
    }
}




Criar TokenAuthenticationFilter



A tarefa desse filtro é extrair o token de acesso do cabeçalho de autorização, se houver, para validá-lo e inicializar o contexto de segurança.



TokenAuthenticationFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final UserService userService;

    private final TokenService tokenService;

    public TokenAuthenticationFilter(
            UserService userService, TokenService tokenService) {
        this.userService = requireNonNull(userService);
        this.tokenService = requireNonNull(tokenService);
    }

    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws ServletException, IOException {
        try {
            Optional<String> jwtOpt = getJwtFromRequest(request);
            if (jwtOpt.isPresent()) {
                String jwt = jwtOpt.get();
                if (isNotEmpty(jwt) && tokenService.isValidAccessToken(jwt)) {
                    String login = tokenService.getUsername(jwt);
                    Optional<User> userOpt = userService.findByLogin(login);
                    if (userOpt.isPresent()) {
                        User user = userOpt.get();
                        ExampleOAuth2User oAuth2User = new ExampleOAuth2User(user);
                        OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(oAuth2User, oAuth2User.getAuthorities(), oAuth2User.getProvider());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Could not set user authentication in security context", e);
        }

        chain.doFilter(request, response);
    }

    private Optional<String> getJwtFromRequest(HttpServletRequest request) {
        String token = request.getHeader(AUTHORIZATION);
        if (isNotEmpty(token) && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        return Optional.ofNullable(token);
    }
}




Criar ponto de extremidade de token de atualização



Por motivos de segurança, a vida útil do token de acesso geralmente é mantida pequena. Então, se for roubado, um invasor não poderá usá-lo indefinidamente. Para não forçar o usuário a fazer login no aplicativo repetidamente, o token de atualização é usado. Ele é emitido pelo servidor após a autenticação bem-sucedida junto com o token de acesso e tem uma vida útil mais longa. Usando-o, você pode solicitar um novo par de tokens. Recomenda-se armazenar o token de atualização no cookie httpOnly.



Atualizar endpoint de token
@Path("/auth")
public class AuthEndpoint extends EndpointBase {

...

    @POST
    @Path("/token")
    @Produces(APPLICATION_JSON)
    public Response refreshToken(@CookieParam(REFRESH_TOKEN) String refreshToken) {
        return handle(() -> {
            if (refreshToken == null) {
                throw new InvalidTokenException("Refresh token was not provided.");
            }
            RefreshToken oldRefreshToken = tokenService.findRefreshToken(refreshToken);
            if (oldRefreshToken == null || !tokenService.isValidRefreshToken(oldRefreshToken)) {
                throw new InvalidTokenException("Refresh token is not valid or expired.");
            }

            Map<String, String> result = new HashMap<>();
            result.put("token", tokenService.newAccessToken(of(oldRefreshToken.getUser())));

            RefreshToken newRefreshToken = newRefreshTokenFor(oldRefreshToken.getUser());
            return Response.ok(result).cookie(createRefreshTokenCookie(newRefreshToken)).build();
        });
    }
}




Substituir AuthorizationRequestRepository



Spring Security usa o objeto AuthorizationRequestRepository para armazenar objetos OAuth2AuthorizationRequest durante o processo de autenticação. A implementação padrão é a classe HttpSessionOAuth2AuthorizationRequestRepository, que usa a sessão HTTP como um repositório. Porque nosso serviço não deve armazenar o estado, esta implementação não nos convém. Vamos implementar nossa própria classe que usará cookies HTTP.



HttpCookieOAuth2AuthorizationRequestRepository
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {

    private static final int COOKIE_EXPIRE_SECONDS = 180;

    private static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "OAUTH2-AUTH-REQUEST";

    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        return getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
                .map(cookie -> deserialize(cookie, OAuth2AuthorizationRequest.class))
                .orElse(null);
    }

    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            removeAuthorizationRequestCookies(request, response);
            return;
        }

        addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
        String redirectUriAfterLogin = request.getParameter(QueryParams.REDIRECT_URI);
        if (isNotBlank(redirectUriAfterLogin)) {
            addCookie(response, REDIRECT_URI, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS);
        }
    }

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
        return loadAuthorizationRequest(request);
    }

    public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
        deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        deleteCookie(request, response, REDIRECT_URI);
    }

    private static String serialize(Object object) {
        return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));
    }

    @SuppressWarnings("SameParameterValue")
    private static <T> T deserialize(Cookie cookie, Class<T> clazz) {
        return clazz.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
    }
}




Configurando Spring Security



Vamos juntar todos os itens acima e configurar o Spring Security.



WebSecurityConfig
@Configuration
@EnableWebSecurity
public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final ExampleOAuth2UserService userService;

    private final TokenAuthenticationFilter tokenAuthenticationFilter;

    private final AuthenticationFailureHandler authenticationFailureHandler;

    private final AuthenticationSuccessHandler authenticationSuccessHandler;

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    @Autowired
    public WebSecurityConfig(
            ExampleOAuth2UserService userService,
            TokenAuthenticationFilter tokenAuthenticationFilter,
            AuthenticationFailureHandler authenticationFailureHandler,
            AuthenticationSuccessHandler authenticationSuccessHandler,
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.userService = userService;
        this.tokenAuthenticationFilter = tokenAuthenticationFilter;
        this.authenticationFailureHandler = authenticationFailureHandler;
        this.authenticationSuccessHandler = authenticationSuccessHandler;
        this.authorizationRequestRepository = authorizationRequestRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .formLogin().disable()
                .httpBasic().disable()
                .sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS))
                .exceptionHandling(eh -> eh
                        .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                )
                .authorizeRequests(authorizeRequests -> authorizeRequests
                        .antMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2Login -> oauth2Login
                        .failureHandler(authenticationFailureHandler)
                        .successHandler(authenticationSuccessHandler)
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(userService))
                        .authorizationEndpoint(authEndpoint -> authEndpoint.authorizationRequestRepository(authorizationRequestRepository))
                );

        http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}




Criar ponto final de repositórios



Para que autenticação por meio de OAuth2 e Bitbucket era necessária - a capacidade de usar a API Bitbucket para acessar seus recursos. Usamos a API de repositórios do Bitbucket para obter uma lista dos repositórios do usuário atual.



Endpoint de repositórios
@Path("/api")
public class ApiEndpoint extends EndpointBase {

    @Autowired
    private BitbucketService bitbucketService;

    @GET
    @Path("/repositories")
    @Produces(APPLICATION_JSON)
    public List<Repository> getRepositories() {
        return handle(bitbucketService::getRepositories);
    }
}

public class BitbucketServiceImpl implements BitbucketService {

    private static final String BASE_URL = "https://api.bitbucket.org";

    private final Supplier<RestTemplate> restTemplate;

    public BitbucketServiceImpl(Supplier<RestTemplate> restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public List<Repository> getRepositories() {
        UriComponentsBuilder uriBuilder = fromHttpUrl(format("%s/2.0/repositories", BASE_URL));
        uriBuilder.queryParam("role", "member");

        ResponseEntity<BitbucketRepositoriesResponse> response = restTemplate.get().exchange(
                uriBuilder.toUriString(),
                HttpMethod.GET,
                new HttpEntity<>(new HttpHeadersBuilder()
                        .acceptJson()
                        .build()),
                BitbucketRepositoriesResponse.class);

        BitbucketRepositoriesResponse body = response.getBody();
        return body == null ? emptyList() : extractRepositories(body);
    }

    private List<Repository> extractRepositories(BitbucketRepositoriesResponse response) {
        return response.getValues() == null
                ? emptyList()
                : response.getValues().stream().map(BitbucketServiceImpl.this::convertRepository).collect(toList());
    }

    private Repository convertRepository(BitbucketRepository bbRepo) {
        Repository repo = new Repository();
        repo.setId(bbRepo.getUuid());
        repo.setFullName(bbRepo.getFullName());
        return repo;
    }
}




Testando



Para o teste, precisamos de um pequeno servidor HTTP para enviar um token de acesso. Primeiro, vamos tentar chamar o endpoint do repositório sem um token de acesso e ter certeza de obter um erro 401 neste caso. Em seguida, vamos autenticar. Para fazer isso, inicie o servidor e vá para o navegador em http: // localhost: 8080 / auth / login . Depois de inserir o nome de usuário / senha, o cliente receberá um token e chamará o endpoint do repositório novamente. Em seguida, um novo token será solicitado e o endpoint do repositório será chamado novamente com o novo token.



OAuth2JwtExampleClient
public class OAuth2JwtExampleClient {

    /**
     * Start client, then navigate to http://localhost:8080/auth/login.
     */
    public static void main(String[] args) throws Exception {
        AuthCallbackHandler authEndpoint = new AuthCallbackHandler(8081);
        authEndpoint.start(SOCKET_READ_TIMEOUT, true);

        HttpResponse response = getRepositories(null);
        assert (response.getStatusLine().getStatusCode() == SC_UNAUTHORIZED);

        Tokens tokens = authEndpoint.getTokens();
        System.out.println("Received tokens: " + tokens);
        response = getRepositories(tokens.getAccessToken());
        assert (response.getStatusLine().getStatusCode() == SC_OK);
        System.out.println("Repositories: " + IOUtils.toString(response.getEntity().getContent(), UTF_8));

        // emulate token usage - wait for some time until iat and exp attributes get updated
        // otherwise we will receive the same token
        Thread.sleep(5000);

        tokens = refreshToken(tokens.getRefreshToken());
        System.out.println("Refreshed tokens: " + tokens);

        // use refreshed token
        response = getRepositories(tokens.getAccessToken());
        assert (response.getStatusLine().getStatusCode() == SC_OK);
    }

    private static Tokens refreshToken(String refreshToken) throws IOException {
        BasicClientCookie cookie = new BasicClientCookie(REFRESH_TOKEN, refreshToken);
        cookie.setPath("/");
        cookie.setDomain("localhost");
        BasicCookieStore cookieStore = new BasicCookieStore();
        cookieStore.addCookie(cookie);

        HttpPost request = new HttpPost("http://localhost:8080/auth/token");
        request.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());

        HttpClient httpClient = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build();
        HttpResponse execute = httpClient.execute(request);

        Gson gson = new Gson();
        Type type = new TypeToken<Map<String, String>>() {
        }.getType();
        Map<String, String> response = gson.fromJson(IOUtils.toString(execute.getEntity().getContent(), UTF_8), type);

        Cookie refreshTokenCookie = cookieStore.getCookies().stream()
                .filter(c -> REFRESH_TOKEN.equals(c.getName()))
                .findAny()
                .orElseThrow(() -> new IOException("Refresh token cookie not found."));
        return Tokens.of(response.get("token"), refreshTokenCookie.getValue());
    }

    private static HttpResponse getRepositories(String accessToken) throws IOException {
        HttpClient httpClient = HttpClientBuilder.create().build();
        HttpGet request = new HttpGet("http://localhost:8080/api/repositories");
        request.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());
        if (accessToken != null) {
            request.setHeader(AUTHORIZATION, "Bearer " + accessToken);
        }
        return httpClient.execute(request);
    }
}




Saída do console do cliente.



Received tokens: Tokens(accessToken=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJldm9sdmVjaS10ZXN0a2l0IiwidXNlcm5hbWUiOiJFdm9sdmVDSSBUZXN0a2l0IiwiaWF0IjoxNjA1NDY2MDMxLCJleHAiOjE2MDU0NjY2MzF9.UuRYMdIxzc8ZFEI2z8fAgLz-LG_gDxaim25pMh9jNrDFK6YkEaDqDO8Huoav5JUB0bJyf1lTB0nNPaLLpOj4hw, refreshToken=BBF6dboG8tB4XozHqmZE5anXMHeNUncTVD8CLv2hkaU2KsfyqitlJpgkV4HrQqPk)

Repositories: [{"id":"{c7bb4165-92f1-4621-9039-bb1b6a74488e}","fullName":"test-namespace/test-repository1"},{"id":"{aa149604-c136-41e1-b7bd-3088fb73f1b2}","fullName":"test-namespace/test-repository2"}]

Refreshed tokens: Tokens(accessToken=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJldm9sdmVjaS10ZXN0a2l0IiwidXNlcm5hbWUiOiJFdm9sdmVDSSBUZXN0a2l0IiwiaWF0IjoxNjA1NDY2MDM2LCJleHAiOjE2MDU0NjY2MzZ9.oR2A_9k4fB7qpzxvV5QKY1eU_8aZMYEom-ngc4Kuc5omeGPWyclfqmiyQTpJW_cHOcXbY9S065AE_GKXFMbh_Q, refreshToken=mdc5sgmtiwLD1uryubd2WZNjNzSmc5UGo6JyyzsiYsBgOpeaY3yw3T3l8IKauKYQ)


Fonte



O código-fonte completo do aplicativo analisado está no Github .



Links





PS

O serviço REST que criamos é executado no protocolo HTTP para não complicar o exemplo. Mas, como nossos tokens não são criptografados de forma alguma, é recomendável mudar para um canal seguro (HTTPS).



All Articles