Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Deploy Auth to ECS

on:
push:
branches:
- main
workflow_dispatch:

env:
AWS_REGION: ap-northeast-2
AWS_ACCOUNT_ID: 727452759104
ECR_REPOSITORY: momentlit/auth
ECS_CLUSTER: default
ECS_SERVICE: momentlit-auth-service
IMAGE_TAG: latest

jobs:
deploy:
name: Build and Deploy Auth
runs-on: ubuntu-latest

steps:
- name: Checkout source code
uses: actions/checkout@v4
Comment on lines +17 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

보안 강화: permissions 블록 추가 및 persist-credentials: false 설정 필요

정적 분석에서 지적한 대로, 최소 권한 원칙을 위해 명시적 권한 블록을 추가하고, checkout 단계에서 자격 증명 지속을 비활성화해야 합니다.

🛡️ 권장 수정 사항
 jobs:
   deploy:
     name: Build and Deploy Auth
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      id-token: write

     steps:
       - name: Checkout source code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jobs:
deploy:
name: Build and Deploy Auth
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
jobs:
deploy:
name: Build and Deploy Auth
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 23-24: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 17 - 24, The GitHub Actions
workflow needs security hardening to follow the principle of least privilege.
Add a top-level permissions block to the workflow (at the same level as the jobs
section) to explicitly declare minimal required permissions, and then add
persist-credentials: false parameter to the checkout@v4 step to prevent Git
credentials from being stored in the runner environment. This ensures the
workflow only requests and retains the minimum permissions necessary for the
build and deploy operation.

Source: Linters/SAST tools


- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
aws-region: ${{ env.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push Docker image
run: |
IMAGE_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG}

docker buildx build \
--platform linux/amd64 \
--provenance=false \
-t $IMAGE_URI \
. \
--push

- name: Force new ECS deployment
run: |
aws ecs update-service \
--cluster $ECS_CLUSTER \
--service $ECS_SERVICE \
--force-new-deployment \
--region $AWS_REGION

- name: Wait for ECS service stable
run: |
aws ecs wait services-stable \
--cluster $ECS_CLUSTER \
--services $ECS_SERVICE \
--region $AWS_REGION
52 changes: 50 additions & 2 deletions API_SPEC.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,54 @@ servers:
tags: []
paths: {}
components:
schemas: {}
responses: {}
schemas:
ErrorResponse:
type: object
properties:
message:
type: string
example: "[ERROR: Request/BadRequest] Refresh Token을 입력해주세요."
data:
nullable: true
example: null
required:
- message
- data
Comment on lines +13 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

에러 응답 스키마가 코딩 가이드라인을 위반합니다.

코딩 가이드라인은 "API error responses should have a consistent format with 'code' and 'message' fields"를 요구하지만, 현재 ErrorResponse 스키마는 code 필드가 없고 messagedata 필드만 있습니다. 에러 코드가 메시지 문자열에 접두사로 포함되어 있어(예: "[ERROR: Request/BadRequest]") 클라이언트가 구조화된 데이터 대신 문자열 파싱을 해야 합니다.

다음과 같이 구조화된 스키마로 변경하는 것을 권장합니다:

ErrorResponse:
  type: object
  properties:
    code:
      type: string
      example: "REQUEST_BAD_REQUEST"
    message:
      type: string
      example: "Refresh Token을 입력해주세요."
    data:
      nullable: true
      example: null
  required:
    - code
    - message

이 변경은 ApiResponse DTO와 GlobalExceptionHandler의 응답 생성 로직도 함께 수정해야 합니다.

As per coding guidelines, "API error responses should have a consistent format with 'code' and 'message' fields."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@API_SPEC.yaml` around lines 13 - 24, The ErrorResponse schema in
API_SPEC.yaml violates the coding guideline requiring error responses to have
both 'code' and 'message' fields as separate properties. Currently, the error
code is embedded as a string prefix in the 'message' field (e.g., "[ERROR:
Request/BadRequest]"), forcing clients to parse strings instead of using
structured data. Add a new 'code' property to the ErrorResponse schema as a
string type, remove the error code prefix from the message example, and update
the 'required' array to include both 'code' and 'message'. Additionally, ensure
that the ApiResponse DTO class and GlobalExceptionHandler's response creation
logic are modified to populate the separate 'code' and 'message' fields
independently rather than combining them into a single message string.

Source: Coding guidelines

responses:
BadRequestError:
description: "Invalid request input."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: Request/BadRequest] Refresh Token을 입력해주세요."
data: null
UnauthorizedError:
description: "Authentication failed or token is invalid."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: Auth/Unauthorized] 유효하지 않은 Refresh Token입니다."
data: null
GoogleOauthError:
description: "Google OAuth request failed."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: Auth/Oauth/Google] Google Access Token을 발급받을 수 없습니다."
data: null
InternalServerError:
description: "Unexpected server error."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: ?/?] 서버 내부 오류가 발생했습니다."
data: null
Comment on lines +25 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

에러 응답 컴포넌트의 예시가 구조화되지 않은 에러 코드를 포함합니다.

각 에러 응답 예시의 message 필드에 "[ERROR: Request/BadRequest]" 같은 접두사가 포함되어 있습니다. 위에서 언급한 대로 code 필드를 별도로 분리하면, 예시도 다음과 같이 업데이트되어야 합니다:

BadRequestError:
  description: "Invalid request input."
  content:
    application/json:
      schema:
        $ref: "`#/components/schemas/ErrorResponse`"
      example:
        code: "REQUEST_BAD_REQUEST"
        message: "Refresh Token을 입력해주세요."
        data: null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@API_SPEC.yaml` around lines 25 - 61, The error response examples in the
responses section (BadRequestError, UnauthorizedError, GoogleOauthError,
InternalServerError) contain error codes embedded in the message field as
prefixes like "[ERROR: Request/BadRequest]". Extract these error code prefixes
from the message field and create a separate `code` field for each error
response example. Update BadRequestError to have code "REQUEST_BAD_REQUEST",
UnauthorizedError to have code "AUTH_UNAUTHORIZED", GoogleOauthError to have
code "AUTH_OAUTH_GOOGLE", and InternalServerError to have an appropriate code
value. Keep only the Korean error message text (without the prefix) in the
message field for each example.

Source: Coding guidelines

securitySchemes: {}
3 changes: 2 additions & 1 deletion docs/service-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ No repository or persistence component is visible. Build dependencies include Sp

## Exception Handling

No project-specific exception handling is visible.
Project-specific exception handling is implemented under `com.example.auth.global.exception`.
Auth-specific exceptions extend `AuthException` and are handled by `GlobalExceptionHandler`.

## Test Structure

Expand Down
10 changes: 9 additions & 1 deletion docs/service-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ State transitions are documented only where visible in entity or service methods

## Exception Cases

No project-specific exception handling is visible. HTTP status mapping for these exceptions is Needs confirmation unless explicitly handled in code.
Project-specific exception handling is visible under `com.example.auth.global.exception`.
Visible mappings:

- `BadRequestException`: `400 Bad Request`
- `UnauthorizedException`: `401 Unauthorized`
- `TokenNotFoundException`: `401 Unauthorized`
- `GoogleOauthException`: `502 Bad Gateway`
- Other `AuthException`: `500 Internal Server Error`
- Other `Exception`: `500 Internal Server Error`

## API Behavior Policy

Expand Down
113 changes: 100 additions & 13 deletions src/main/java/com/example/auth/global/client/UserServiceClient.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
package com.example.auth.global.client;

import com.example.auth.dto.request.SignInRequest;
import com.example.auth.global.client.dto.request.UserGoogleOauthRequest;
import com.example.auth.global.client.dto.response.GoogleUserInfoResponse;
import com.example.auth.global.client.dto.response.UserAuthResponse;
import com.example.auth.global.client.dto.request.UserGoogleOauthRequest;
import com.example.auth.global.exception.DownstreamServiceException;
import com.example.auth.global.exception.UnauthorizedException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestClientResponseException;

@Slf4j
@Component
public class UserServiceClient {

private static final String SERVICE_NAME = "USER";

private final RestClient restClient;
private final ObjectMapper objectMapper = new ObjectMapper();

public UserServiceClient(
@Value("${user-service.base-url:http://localhost:8081}") String userServiceBaseUrl //임시
@Value("${user-service.base-url:http://localhost:8081}") String userServiceBaseUrl
) {
this.restClient = RestClient.builder()
.baseUrl(userServiceBaseUrl)
.build();
}

public UserAuthResponse authenticate(SignInRequest request) {
return restClient.post()
.uri("/internal/users/authenticate")
.body(request)
.retrieve()
.body(UserAuthResponse.class);
try {
return restClient.post()
.uri("/internal/users/authenticate")
.body(request)
.retrieve()
.body(UserAuthResponse.class);

} catch (RestClientResponseException e) {
if (e.getStatusCode().isSameCodeAs(HttpStatus.UNAUTHORIZED)) {
throw new UnauthorizedException("이메일 또는 비밀번호가 일치하지 않습니다.");
}

throw convertToDownstreamException(e);

} catch (RestClientException e) {
throw convertToConnectionException(e);
}
}

public UserAuthResponse authenticateGoogle(GoogleUserInfoResponse request) {
Expand All @@ -38,10 +63,72 @@ public UserAuthResponse authenticateGoogle(GoogleUserInfoResponse request) {
request.imageUrl()
);

return restClient.post()
.uri("/internal/users/oauth/google")
.body(userRequest)
.retrieve()
.body(UserAuthResponse.class);
try {
return restClient.post()
.uri("/internal/users/oauth/google")
.body(userRequest)
.retrieve()
.body(UserAuthResponse.class);

} catch (RestClientResponseException e) {
throw convertToDownstreamException(e);

} catch (RestClientException e) {
throw convertToConnectionException(e);
}
}

private DownstreamServiceException convertToDownstreamException(RestClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
String message = extractMessage(responseBody);
HttpStatusCode statusCode = e.getStatusCode();

if (statusCode.is4xxClientError()) {
log.warn(
"{} service client error. status={}, body={}",
SERVICE_NAME,
statusCode,
responseBody
);
} else {
log.error(
"{} service server error. status={}, body={}",
SERVICE_NAME,
statusCode,
responseBody
);
}

return new DownstreamServiceException(
SERVICE_NAME,
statusCode,
message,
responseBody
);
}

private DownstreamServiceException convertToConnectionException(RestClientException e) {
log.error("{} service connection failed", SERVICE_NAME, e);

return new DownstreamServiceException(
SERVICE_NAME,
HttpStatus.SERVICE_UNAVAILABLE,
"USER 서비스에 연결할 수 없습니다.",
null
);
}

private String extractMessage(String responseBody) {
try {
JsonNode jsonNode = objectMapper.readTree(responseBody);

if (jsonNode.has("message")) {
return jsonNode.get("message").asText();
}

return "USER 서비스 호출 중 오류가 발생했습니다.";
} catch (Exception e) {
return "USER 서비스 호출 중 오류가 발생했습니다.";
}
}
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/example/auth/global/dto/ApiResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
public record ApiResponse<T> (
String message,
T data
){}
){
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(message, null);
}
Comment on lines 3 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

에러 응답 포맷을 code/message 분리 구조로 맞춰주세요.

현재 fail(String message)messagedata=null만 반환해서, 에러 코드를 문자열 접두사로 붙이는 구현에 의존하게 됩니다. 이 구조는 전역 예외 응답의 계약을 불안정하게 만들고, 클라이언트가 code를 구조적으로 파싱할 수 없습니다.

변경 예시
-public record ApiResponse<T> (
-    String message,
-    T data
-){
-    public static <T> ApiResponse<T> fail(String message) {
-        return new ApiResponse<>(message, null);
-    }
+public record ApiResponse<T> (
+    String code,
+    String message,
+    T data
+){
+    public static <T> ApiResponse<T> fail(String code, String message) {
+        return new ApiResponse<>(code, message, null);
+    }
 }

As per coding guidelines, "API error responses should have a consistent format with 'code' and 'message' fields."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/auth/global/dto/ApiResponse.java` around lines 3 -
9, The ApiResponse record currently lacks a code field, and the fail method only
accepts a message parameter with error codes embedded as string prefixes. Add a
code field to the ApiResponse record to store error codes separately, then
update the fail method signature to accept both code and message as distinct
parameters. This will ensure error responses follow a consistent structure with
properly separated code and message fields, allowing clients to parse error
codes structurally rather than relying on string parsing.

Source: Coding guidelines

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.auth.global.exception;

public class AuthException extends RuntimeException {
public AuthException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.auth.global.exception;

public class BadRequestException extends AuthException {
public BadRequestException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.auth.global.exception;

import lombok.Getter;
import org.springframework.http.HttpStatusCode;

@Getter
public class DownstreamServiceException extends RuntimeException {

private final String serviceName;
private final HttpStatusCode statusCode;
private final String responseBody;

public DownstreamServiceException(
String serviceName,
HttpStatusCode statusCode,
String message,
String responseBody
) {
super(message);
this.serviceName = serviceName;
this.statusCode = statusCode;
this.responseBody = responseBody;
}
}
Loading