1. 개요

이 사용방법(예제)에서는 Jakarta EE 및 MicroProfile을 사용하여 OAuth 2.0 권한 부여 프레임워크 에 대한 구현을 제공할 것입니다. 가장 중요한 것은 Authorization Code 부여 유형 을 통해 OAuth 2.0 역할 의 상호 작용을 구현할 것 입니다. 이 글을 쓰는 동기는 아직 OAuth에 대한 지원을 제공하지 않는 Jakarta EE를 사용하여 구현되는 프로젝트에 대한 지원을 제공하는 것입니다.

가장 중요한 역할인 권한 부여 서버의 경우 권한 부여 Endpoints, 토큰 Endpoints 및 추가 로 리소스 서버가 공개 키를 검색하는 데 유용한 JWK 키 Endpoints을 구현할 것 입니다.

빠른 설정을 위해 구현이 간단하고 쉽기를 원하므로 미리 등록된 클라이언트 및 사용자 저장소와 액세스 토큰용 JWT 저장소를 사용할 것입니다.

주제로 바로 이동하기 전에 이 사용방법(예제)의 예제는 교육용이라는 점에 유의해야 합니다. 프로덕션 시스템의 경우 Keycloak 과 같이 성숙하고 잘 테스트된 솔루션을 사용하는 것이 좋습니다 .

2. OAuth 2.0 개요

이 섹션에서는 OAuth 2.0 역할 및 인증 코드 부여 흐름에 대한 간략한 개요를 제공합니다.

2.1. 역할

OAuth 2.0 프레임워크는 다음 네 가지 역할 간의 협업을 의미합니다.

  • 리소스 소유자 : 일반적으로 최종 사용자입니다. 보호할 가치가 있는 일부 리소스가 있는 엔터티입니다.
  • 리소스 서버 : 리소스 소유자의 데이터를 보호하는 서비스로, 일반적으로 REST API를 통해 게시합니다.
  • 클라이언트 : 리소스 소유자의 데이터를 사용하는 애플리케이션
  • 권한 부여 서버 : 만료되는 토큰의 형태로 클라이언트에 권한 또는 권한을 부여하는 애플리케이션

2.2. 권한 부여 유형

부여 유형  은 클라이언트가 궁극적으로 액세스 토큰의 형태로 리소스 소유자의 데이터를 사용할 수 있는 권한을 얻는 방법입니다

당연히 클라이언트 유형에 따라 선호하는 보조금 유형이 다릅니다 .

  • 인증 코드 : 웹 애플리케이션, 기본 애플리케이션 또는 단일 페이지 애플리케이션 이든  가장 자주 선호  되지만 기본 및 단일 페이지 앱에는 PKCE라는 추가 보호가 필요합니다.
  • Refresh Token : 웹 애플리케이션 이 기존 토큰을 갱신하는 데 적합한 특별 갱신 보조금
  • 클라이언트 자격 증명 : 리소스 소유자가 최종 사용자가 아닌 경우와 같이 서비스 간 통신에 선호 됨
  • 리소스 소유자 비밀번호 : 모바일 앱에 자체 로그인 페이지가 필요한 경우와 같이 네이티브 애플리케이션의 자사 인증에 선호  됩니다 .

또한 클라이언트는 암시적 부여 유형을 사용할 수 있습니다. 그러나 일반적으로 PKCE와 함께 권한 부여 코드 부여를 사용하는 것이 더 안전합니다.

2.3. 인증 코드 부여 흐름

권한 부여 코드 부여 흐름이 가장 일반적이므로 작동 방식도 검토하고 실제로 이 사용방법(예제)에서 빌드할 것입니다.

애플리케이션(클라이언트 )은 인증 서버의 /authorize 엔드포인트로 리디렉션하여 권한을 요청합니다. 이 엔드포인트에 애플리케이션은 콜백 엔드포인트를 제공합니다.

권한 부여 서버는 일반적으로 최종 사용자(자원 소유자)에게 권한을 요청합니다. 최종 사용자가 권한을 부여하면 인증 서버 는  코드 를 사용 하여 콜백으로 다시 리디렉션 합니다 .

애플리케이션은 이 코드를 수신한 다음 인증 서버의  /token  엔드포인트에 대한 인증된 호출을 수행합니다. "인증됨"이란 애플리케이션이 이 호출의 일부로 자신이 누구인지 증명함을 의미합니다. 모두 순서대로 표시되면 인증 서버는 토큰으로 응답합니다.

토큰을 손에 들고 애플리케이션은 API (리소스 서버)에 요청하고 해당 API는 토큰을 확인합니다. /introspect Endpoints 을 사용하여 토큰을 확인하도록 인증 서버에 요청할 수 있습니다 . 또는 토큰이 자체 포함된 경우 리소스 서버는 JWT의 경우와 같이 토큰의 서명을 로컬에서 확인하여 최적화할 수 있습니다.

2.4. Jakarta EE는 무엇을 지원합니까?

아직은 많지 않습니다. 이 사용방법(예제)에서는 처음부터 대부분의 항목을 빌드합니다.

3. OAuth 2.0 인증 서버

이 구현에서는 가장 일반적으로 사용되는 권한 부여 유형 인 인증 코드 에 중점을 둘 것 입니다.

3.1. 클라이언트 및 사용자 등록

물론 인증 서버는 요청을 인증하기 전에 클라이언트와 사용자에 대해 알아야 합니다. 그리고 인증 서버가 이에 대한 UI를 갖는 것이 일반적입니다.

그러나 간단하게 하기 위해 미리 구성된 클라이언트를 사용합니다.

INSERT INTO clients (client_id, client_secret, redirect_uri, scope, authorized_grant_types) 
VALUES ('webappclient', 'webappclientsecret', 'http://localhost:9180/callback', 
  'resource.read resource.write', 'authorization_code refresh_token');
@Entity
@Table(name = "clients")
public class Client {
    @Id
    @Column(name = "client_id")
    private String clientId;
    @Column(name = "client_secret")
    private String clientSecret;

    @Column(name = "redirect_uri")
    private String redirectUri;

    @Column(name = "scope")
    private String scope;

    // ...
}

사전 구성된 사용자:

INSERT INTO users (user_id, password, roles, scopes)
VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity
@Table(name = "users")
public class User implements Principal {
    @Id
    @Column(name = "user_id")
    private String userId;

    @Column(name = "password")
    private String password;

    @Column(name = "roles")
    private String roles;

    @Column(name = "scopes")
    private String scopes;

    // ...
}

이 사용방법(예제)에서는 일반 텍스트로 된 암호를 사용 했지만 프로덕션 환경에서는 해시되어야 합니다 .

이 사용방법(예제)의 나머지 부분 에서는 리소스 소유자인 appuser가 인증 코드를 구현하여 webappclient(애플리케이션)에 대한 액세스 권한을 부여 하는 방법 을 보여줍니다.

3.2. 권한 부여 Endpoints

권한 부여 Endpoints의 주요 역할은 먼저 사용자를 인증한 다음 애플리케이션이 원하는 권한 또는 범위를 요청하는 것 입니다.

OAuth2 사양에서 지시한 대로 이 엔드포인트는 HTTP GET 메서드를 지원해야 하지만 HTTP POST 메서드도 지원할 수 있습니다. 이 구현에서는 HTTP GET 메서드만 지원합니다.

첫째, 인증 엔드포인트는 사용자가 인증되어야 합니다 . 사양에는 여기에서 특정 방식이 필요하지 않으므로 Jakarta EE 8 Security API 의 양식 인증을 사용하겠습니다 .

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)

사용자는 인증을 위해 /login.jsp 로 리디렉션된 다음 SecurityContext API 를 통해 CallerPrincipal 로 사용할 수 있습니다 .

Principal principal = securityContext.getCallerPrincipal();

JAX-RS를 사용하여 다음과 같이 조합할 수 있습니다.

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)
@Path("authorize")
public class AuthorizationEndpoint {
    //...    
    @GET
    @Produces(MediaType.TEXT_HTML)
    public Response doGet(@Context HttpServletRequest request,
      @Context HttpServletResponse response,
      @Context UriInfo uriInfo) throws ServletException, IOException {
        
        MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
        Principal principal = securityContext.getCallerPrincipal();
        // ...
    }
}

이 시점에서 권한 부여 Endpoints은 애플리케이션의 요청 처리를 시작할 수 있습니다. 여기에는 response_typeclient_id 매개변수와 선택 사항이지만 권장되는 redirect_uri, 범위상태 매개변수가 포함되어야 합니다.

client_id 는 클라이언트 데이터베이스 테이블 유효한 클라이언트여야 합니다.

redirect_uri 가 지정된 경우 클라이언트 데이터베이스 테이블 에서 찾은 것과 일치해야 합니다.

그리고 Authorization Code를 수행하고 있기 때문에 response_type코드입니다. 

인증은 다단계 프로세스이므로 세션에 다음 값을 임시로 저장할 수 있습니다.

request.getSession().setAttribute("ORIGINAL_PARAMS", params);

그런 다음 해당 페이지로 리디렉션하여 애플리케이션이 사용할 수 있는 권한을 사용자에게 요청할 준비를 합니다.

String allowedScopes = checkUserScopes(user.getScopes(), requestedScope);
request.setAttribute("scopes", allowedScopes);
request.getRequestDispatcher("/authorize.jsp").forward(request, response);

3.3. 사용자 범위 승인

이 시점에서 브라우저는 사용자를 위한 인증 UI를 렌더링하고 사용자는 선택합니다. 그런 다음 브라우저 는  HTTP POST 에서 사용자의 선택을 제출합니다 .

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Response doPost(@Context HttpServletRequest request, @Context HttpServletResponse response,
  MultivaluedMap<String, String> params) throws Exception {
    MultivaluedMap<String, String> originalParams = 
      (MultivaluedMap<String, String>) request.getSession().getAttribute("ORIGINAL_PARAMS");

    // ...

    String approvalStatus = params.getFirst("approval_status"); // YES OR NO

    // ... if YES

    List<String> approvedScopes = params.get("scope");

    // ...
}

다음으로 user_id, client_id redirect_uri 를 참조하는 임시 코드를 생성합니다. 이 코드는 나중에 애플리케이션이 토큰 엔드포인트에 도달할 때 사용할 것입니다.

이제 자동 생성된 ID를 사용하여 AuthorizationCode JPA 엔티티를 생성해 보겠습니다  .

@Entity
@Table(name ="authorization_code")
public class AuthorizationCode {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "code")
private String code;

//...

}

그런 다음 채웁니다.

AuthorizationCode authorizationCode = new AuthorizationCode();
authorizationCode.setClientId(clientId);
authorizationCode.setUserId(userId);
authorizationCode.setApprovedScopes(String.join(" ", authorizedScopes));
authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(2));
authorizationCode.setRedirectUri(redirectUri);

bean을 저장하면 code 속성이 자동으로 채워지므로 가져와서 클라이언트에 다시 보낼 수 있습니다.

appDataRepository.save(authorizationCode);
String code = authorizationCode.getCode();

인증 코드는 2분 후에 만료됩니다 . 이 만료 시간 은 최대한 신중해야 합니다. 클라이언트가 바로 액세스 토큰으로 교환하기 때문에 짧을 수 있습니다.

그런 다음 애플리케이션의 redirect_uri 로 다시 리디렉션하여 애플리케이션이 /authorize 요청 에 지정한 상태 매개변수 와 코드를 제공합니다 .

StringBuilder sb = new StringBuilder(redirectUri);
// ...

sb.append("?code=").append(code);
String state = params.getFirst("state");
if (state != null) {
    sb.append("&state=").append(state);
}
URI location = UriBuilder.fromUri(sb.toString()).build();
return Response.seeOther(location).build();

redirectUri 는 redirect_uri 요청 매개변수 가 아니라 클라이언트 테이블 에 있는 모든 것입니다 .

따라서 다음 단계는 클라이언트가 이 코드를 수신하고 토큰 Endpoints을 사용하여 액세스 토큰으로 교환하는 것입니다.

3.4. 토큰 Endpoints

인증 엔드포인트와 달리 토큰 엔드포인트 는 클라이언트와 통신하기 위해 브라우저가 필요하지 않으므로 JAX-RS 엔드포인트로 구현합니다.

@Path("token")
public class TokenEndpoint {

    List<String> supportedGrantTypes = Collections.singletonList("authorization_code");

    @Inject
    private AppDataRepository appDataRepository;

    @Inject
    Instance<AuthorizationGrantTypeHandler> authorizationGrantTypeHandlers;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response token(MultivaluedMap<String, String> params,
       @HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) throws JOSEException {
        //...
    }
}

토큰 엔드포인트에는 POST가 필요하고 application/x-www-form-urlencoded 미디어 유형 을 사용하여 매개변수를 인코딩해야 합니다.

논의한 바와 같이 인증 코드 부여 유형만 지원합니다.

List<String> supportedGrantTypes = Collections.singletonList("authorization_code");

따라서 필수 매개변수로 수신 된 grant_type 이 지원되어야 합니다.

String grantType = params.getFirst("grant_type");
Objects.requireNonNull(grantType, "grant_type params is required");
if (!supportedGrantTypes.contains(grantType)) {
    JsonObject error = Json.createObjectBuilder()
      .add("error", "unsupported_grant_type")
      .add("error_description", "grant type should be one of :" + supportedGrantTypes)
      .build();
    return Response.status(Response.Status.BAD_REQUEST)
      .entity(error).build();
}

다음으로 HTTP 기본 인증을 통해 클라이언트 인증을 확인합니다. 즉, Authorization 헤더 통해 받은 client_idclient_secret 이 등록된 클라이언트와 일치하는지 확인합니다.

String[] clientCredentials = extract(authHeader);
String clientId = clientCredentials[0];
String clientSecret = clientCredentials[1];
Client client = appDataRepository.getClient(clientId);
if (client == null || clientSecret == null || !clientSecret.equals(client.getClientSecret())) {
    JsonObject error = Json.createObjectBuilder()
      .add("error", "invalid_client")
      .build();
    return Response.status(Response.Status.UNAUTHORIZED)
      .entity(error).build();
}

마지막으로 TokenResponse 생성 을 해당 부여 유형 핸들러에 Delegation합니다.

public interface AuthorizationGrantTypeHandler {
    TokenResponse createAccessToken(String clientId, MultivaluedMap<String, String> params) throws Exception;
}

권한 부여 코드 부여 유형에 더 관심이 있으므로 CDI 빈으로 적절한 구현을 제공하고 Named 어노테이션 으로 장식했습니다 .

@Named("authorization_code")

런타임 시 수신 된 grant_type 값에 따라 해당 구현이 CDI 인스턴스 메커니즘 을 통해 활성화됩니다 .

String grantType = params.getFirst("grant_type");
//...
AuthorizationGrantTypeHandler authorizationGrantTypeHandler = 
  authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();

이제 /token 의 응답을 생성할 시간입니다.

3.5. RSA 개인 및 공개 키

토큰을 생성하기 전에 토큰 서명을 위한 RSA 개인 키가 필요합니다.

이를 위해 OpenSSL을 사용합니다.

# PRIVATE KEY
openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048

private-key.pemMETA-INF/microprofile-config.properties 파일을 사용하여 MicroProfile Config 서명 키 특성을 통해 서버에 제공됩니다 .

signingkey=/META-INF/private-key.pem

서버는 삽입된 Config 개체 를 사용하여 속성을 읽을 수 있습니다 .

String signingkey = config.getValue("signingkey", String.class);

마찬가지로 해당 공개 키를 생성할 수 있습니다.

# PUBLIC KEY
openssl rsa -pubout -in private-key.pem -out public-key.pem

MicroProfile Config validationKey를 사용하여 읽으 십시오.

verificationkey=/META-INF/public-key.pem

서버는 확인을 위해 리소스 서버에서 사용할 수 있도록 해야 합니다 . 이는 JWK Endpoints을 통해 수행됩니다.

Nimbus JOSE+JWT 는 여기서 큰 도움이 될 수 있는 라이브러리입니다. 먼저 nimbus-jose-jwt 의존성 을 추가해 보겠습니다.

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>7.7</version>
</dependency>

이제 Nimbus의 JWK 지원을 활용하여 엔드포인트를 단순화할 수 있습니다.

@Path("jwk")
@ApplicationScoped
public class JWKEndpoint {

    @GET
    public Response getKey(@QueryParam("format") String format) throws Exception {
        //...

        String verificationkey = config.getValue("verificationkey", String.class);
        String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString(verificationkey);
        if (format == null || format.equals("jwk")) {
            JWK jwk = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey);
            return Response.ok(jwk.toJSONString()).type(MediaType.APPLICATION_JSON).build();
        } else if (format.equals("pem")) {
            return Response.ok(pemEncodedRSAPublicKey).build();
        }

        //...
    }
}

형식 매개변수 를 사용 하여 PEM과 JWK 형식 사이를 전환했습니다. 리소스 서버를 구현하는 데 사용할 MicroProfile JWT는 이 두 형식을 모두 지원합니다.

3.6. 토큰 Endpoints 응답

이제 주어진 AuthorizationGrantTypeHandler 가 토큰 응답을 생성할 때입니다. 이 구현에서는 구조화된 JWT 토큰만 지원합니다.

이 형식으로 토큰을 생성하기 위해 Nimbus JOSE+JWT 라이브러리 를 다시 사용하지만 다른 많은 JWT 라이브러리 도 있습니다.

따라서 서명된 JWT를 생성하려면 먼저 JWT 헤더를 구성해야 합니다.

JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();

그런 다음 표준화 및 사용자 지정 클레임 집합 인 페이로드 를 빌드합니다.

Instant now = Instant.now();
Long expiresInMin = 30L;
Date in30Min = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES));

JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
  .issuer("http://localhost:9080")
  .subject(authorizationCode.getUserId())
  .claim("upn", authorizationCode.getUserId())
  .audience("http://localhost:9280")
  .claim("scope", authorizationCode.getApprovedScopes())
  .claim("groups", Arrays.asList(authorizationCode.getApprovedScopes().split(" ")))
  .expirationTime(in30Min)
  .notBeforeTime(Date.from(now))
  .issueTime(Date.from(now))
  .jwtID(UUID.randomUUID().toString())
  .build();
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);

표준 JWT 클레임 외에도 MicroProfile JWT에 필요한 두 가지 클레임( upngroups )을 추가했습니다. upnJakarta EE Security CallerPrincipal 에 매핑 되고 그룹 은 Jakarta EE 역할에 매핑됩니다.

이제 헤더와 페이로드 가 있으므로 RSA 개인 키로 액세스 토큰에 서명해야 합니다 . 해당 RSA 공개 키는 JWK Endpoints을 통해 노출되거나 리소스 서버가 액세스 토큰을 확인하는 데 사용할 수 있도록 다른 방법으로 제공됩니다.

개인 키를 PEM 형식으로 제공했으므로 이를 검색하여 RSAPrivateKey로 변환해야 합니다.

SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
//...
String signingkey = config.getValue("signingkey", String.class);
String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey);
RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);

다음으로 JWT에 서명하고 직렬화합니다.

signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));
String accessToken = signedJWT.serialize();

마지막으로 토큰 응답을 구성합니다.

return Json.createObjectBuilder()
  .add("token_type", "Bearer")
  .add("access_token", accessToken)
  .add("expires_in", expiresInMin * 60)
  .add("scope", authorizationCode.getApprovedScopes())
  .build();

이는 JSON-P 덕분에 JSON 형식으로 직렬화되어 클라이언트로 전송됩니다.

{
  "access_token": "acb6803a48114d9fb4761e403c17f812",
  "token_type": "Bearer",  
  "expires_in": 1800,
  "scope": "resource.read resource.write"
}

4. OAuth 2.0 클라이언트

이 섹션에서는 Servlet, MicroProfile Config 및 JAX RS 클라이언트 API를 사용하여 웹 기반 OAuth 2.0 클라이언트를 구축합니다 .

보다 정확하게는 두 가지 주요 서블릿을 구현할 것입니다. 하나는 권한 부여 서버의 권한 부여 Endpoints을 요청하고 권한 부여 코드 부여 유형을 사용하여 코드를 가져오는 것이고 다른 하나는 수신된 코드를 사용하고 권한 부여 서버의 토큰 Endpoints에서 액세스 토큰을 요청하는 것입니다. .

또한 새로 고침 토큰 부여 유형을 사용하여 새 액세스 토큰을 얻기 위한 서블릿과 리소스 서버의 API에 액세스하기 위한 서블릿 두 개를 더 구현할 것입니다.

4.1. OAuth 2.0 클라이언트 세부정보

클라이언트가 인증 서버에 이미 등록되어 있으므로 먼저 클라이언트 등록 정보를 제공해야 합니다.

  • client_id: 클라이언트 식별자이며 일반적으로 등록 프로세스 중에 인증 서버에서 발급합니다.
  • client_secret:  클라이언트 암호.
  • redirect_uri: 인증 코드를 받을 위치입니다.
  • 범위: 클라이언트가 권한을 요청했습니다.

또한 클라이언트는 인증 서버의 인증 및 토큰 엔드포인트를 알아야 합니다.

  • authorization_uri: 코드를 가져오는 데 사용할 수 있는 권한 부여 서버 권한 부여 Endpoints의 위치입니다.
  • token_uri: 토큰을 가져오는 데 사용할 수 있는 인증 서버 토큰 Endpoints의 위치입니다.

이 모든 정보는 MicroProfile 구성 파일 META-INF/microprofile-config.properties를 통해 제공됩니다.

# Client registration
client.clientId=webappclient
client.clientSecret=webappclientsecret
client.redirectUri=http://localhost:9180/callback
client.scope=resource.read resource.write

# Provider
provider.authorizationUri=http://127.0.0.1:9080/authorize
provider.tokenUri=http://127.0.0.1:9080/token

4.2. 인증 코드 요청

인증 코드를 가져오는 흐름은 브라우저를 인증 서버의 인증 Endpoints으로 리디렉션하여 클라이언트에서 시작됩니다.

일반적으로 이는 사용자가 인증 없이 또는 클라이언트 /authorize 경로 를 명시적으로 호출하여 보호된 리소스 API에 액세스하려고 할 때 발생합니다 .

@WebServlet(urlPatterns = "/authorize")
public class AuthorizationCodeServlet extends HttpServlet {

    @Inject
    private Config config;

    @Override
    protected void doGet(HttpServletRequest request, 
      HttpServletResponse response) throws ServletException, IOException {
        //...
    }
}

doGet() 메서드 에서 Security 상태 값을 생성하고 저장하는 것으로 시작합니다.

String state = UUID.randomUUID().toString();
request.getSession().setAttribute("CLIENT_LOCAL_STATE", state);

그런 다음 클라이언트 구성 정보를 검색합니다.

String authorizationUri = config.getValue("provider.authorizationUri", String.class);
String clientId = config.getValue("client.clientId", String.class);
String redirectUri = config.getValue("client.redirectUri", String.class);
String scope = config.getValue("client.scope", String.class);

그런 다음 이러한 정보를 인증 서버의 인증 엔드포인트에 쿼리 매개변수로 추가합니다.

String authorizationLocation = authorizationUri + "?response_type=code"
  + "&client_id=" + clientId
  + "&redirect_uri=" + redirectUri
  + "&scope=" + scope
  + "&state=" + state;

마지막으로 브라우저를 다음 URL로 리디렉션합니다.

response.sendRedirect(authorizationLocation);

요청을 처리한 후 권한 부여 서버의 권한 부여 엔드포인트는 수신된 상태 매개변수 외에 코드를 생성하여 redirect_uri 에 추가 하고 브라우저 http://localhost:9081/callback?code=A123&state=Y 로 다시 리디렉션 합니다.

4.3. 액세스 토큰 요청

클라이언트 콜백 서블릿인 /callback 은 수신된 상태를 확인하는 것으로 시작합니다.

String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE");
if (!localState.equals(request.getParameter("state"))) {
    request.setAttribute("error", "The state attribute doesn't match!");
    dispatch("/", request, response);
    return;
}

다음으로 이전에 받은 코드를 사용 하여 인증 서버의 토큰 Endpoints을 통해 액세스 토큰을 요청합니다.

String code = request.getParameter("code");
Client client = ClientBuilder.newClient();
WebTarget target = client.target(config.getValue("provider.tokenUri", String.class));

Form form = new Form();
form.param("grant_type", "authorization_code");
form.param("code", code);
form.param("redirect_uri", config.getValue("client.redirectUri", String.class));

TokenResponse tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE)
  .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue())
  .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenResponse.class);

보시다시피 이 호출에 대한 브라우저 상호 작용이 없으며 HTTP POST로 JAX-RS 클라이언트 API를 사용하여 직접 요청이 이루어집니다.

토큰 Endpoints에는 클라이언트 인증이 필요하므로 Authorization 헤더 에 클라이언트 자격 증명 client_idclient_secret 을 포함했습니다.

클라이언트는 이 액세스 토큰을 사용하여 다음 하위 섹션의 주제인 리소스 서버 API를 호출할 수 있습니다.

4.4. 보호된 리소스 액세스

이 시점에서 유효한 액세스 토큰이 있고 리소스 서버의 / 읽기 및 / 쓰기 API를 호출할 수 있습니다.

이를 위해서는 Authorization 헤더 를 제공해야 합니다 . JAX-RS 클라이언트 API를 사용하면 Invocation.Builder header() 메서드 를 통해 간단하게 수행됩니다 .

resourceWebTarget = webTarget.path("resource/read");
Invocation.Builder invocationBuilder = resourceWebTarget.request();
response = invocationBuilder
  .header("authorization", tokenResponse.getString("access_token"))
  .get(String.class);

5. OAuth 2.0 리소스 서버

이 섹션에서는 JAX-RS, MicroProfile JWT 및 MicroProfile Config를 기반으로 Security 웹 애플리케이션을 구축합니다. MicroProfile JWT는 수신된 JWT의 유효성을 검사하고 JWT 범위를 Jakarta EE 역할에 매핑합니다 .

5.1. 메이븐 의존성

Java EE 웹 API 의존성 외에도 MicroProfile ConfigMicroProfile JWT API도 필요합니다.

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-web-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.eclipse.microprofile.config</groupId>
    <artifactId>microprofile-config-api</artifactId>
    <version>1.3</version>
</dependency>
<dependency>
    <groupId>org.eclipse.microprofile.jwt</groupId>
    <artifactId>microprofile-jwt-auth-api</artifactId>
    <version>1.1</version>
</dependency>

5.2. JWT 인증 메커니즘

MicroProfile JWT는 전달자 토큰 인증 메커니즘의 구현을 제공합니다. 이것은 Authorization 헤더 에 있는 JWT 처리를 처리하고 JWT 클레임을 보유 하는 JsonWebToken 으로 Jakarta EE Security Principal을 사용할 수 있게 하며 범위를 Jakarta EE 역할에 매핑합니다. 자세한 배경 정보는 Jakarta EE Security API 를 살펴보세요 .

서버에서 JWT 인증 메커니즘 을 사용하려면 JAX-RS 애플리케이션에 LoginConfig 어노테이션 을 추가 해야 합니다 .

@ApplicationPath("/api")
@DeclareRoles({"resource.read", "resource.write"})
@LoginConfig(authMethod = "MP-JWT")
public class OAuth2ResourceServerApplication extends Application {
}

또한 MicroProfile JWT는 JWT 서명을 확인하기 위해 RSA 공개 키가 필요합니다 . 인트로스펙션을 통해 또는 간단하게 인증 서버에서 수동으로 키를 복사하여 이를 제공할 수 있습니다. 두 경우 모두 공개 키의 위치를 ​​제공해야 합니다.

mp.jwt.verify.publickey.location=/META-INF/public-key.pem

마지막으로 MicroProfile JWT는 들어오는 JWT의 iss 클레임을 확인해야 합니다. 이는 존재하고 MicroProfile Config 속성의 값과 일치해야 합니다.

mp.jwt.verify.issuer=http://127.0.0.1:9080

일반적으로 이것은 권한 부여 서버의 위치입니다.

5.3. 안전한 Endpoints

데모 목적으로 두 개의 엔드포인트가 있는 리소스 API를 추가합니다. 하나는 resource.read 범위를 가진 사용자가 액세스할 수 있는 읽기 Endpoints이고 다른 하나는 resource.write 범위 를 가진 사용자를 위한 쓰기 Endpoints입니다 .

범위에 대한 제한은 @RolesAllowed 어노테이션 을 통해 수행됩니다 .

@Path("/resource")
@RequestScoped
public class ProtectedResource {

    @Inject
    private JsonWebToken principal;

    @GET
    @RolesAllowed("resource.read")
    @Path("/read")
    public String read() {
        return "Protected Resource accessed by : " + principal.getName();
    }

    @POST
    @RolesAllowed("resource.write")
    @Path("/write")
    public String write() {
        return "Protected Resource accessed by : " + principal.getName();
    }
}

6. 모든 서버 실행

하나의 서버를 실행하려면 해당 디렉토리에서 Maven 명령을 호출하기만 하면 됩니다.

mvn package liberty:run-server

인증 서버, 클라이언트 및 리소스 서버는 각각 다음 위치에서 실행되고 사용 가능합니다.

# Authorization Server
http://localhost:9080/

# Client
http://localhost:9180/

# Resource Server
http://localhost:9280/

따라서 클라이언트 홈 페이지에 액세스한 다음 "액세스 토큰 가져오기"를 클릭하여 인증 흐름을 시작할 수 있습니다. 액세스 토큰을 받은 후 리소스 서버의 읽기쓰기 API에 액세스할 수 있습니다.

부여된 범위에 따라 리소스 서버는 성공적인 메시지로 응답하거나 HTTP 403 금지 상태를 받게 됩니다.

7. 결론

이 기사에서는 호환 가능한 모든 OAuth 2.0 클라이언트 및 리소스 서버와 함께 사용할 수 있는 OAuth 2.0 인증 서버 구현을 제공했습니다.

전체 프레임워크를 설명하기 위해 클라이언트 및 리소스 서버에 대한 구현도 제공했습니다. 이러한 모든 구성 요소를 구현하기 위해 Jakarta EE 8 API, 특히 CDI, Servlet, JAX RS, Jakarta EE Security를 ​​사용했습니다. 또한 MicroProfile: MicroProfile Config 및 MicroProfile JWT의 pseudo-Jakarta EE API를 사용했습니다.

예제의 전체 소스 코드는 GitHub에서 사용할 수 있습니다 . 코드에는 권한 부여 코드 및 새로 고침 토큰 부여 유형의 예가 모두 포함되어 있습니다.

마지막으로, 이 기사의 교육적 성격과 주어진 예제가 프로덕션 시스템에서 사용되어서는 안 된다는 점을 인식하는 것이 중요합니다.

Security footer banner