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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

불변 이미지로 배포하고 새 task definition revision을 지정하세요.

latest를 덮어쓴 뒤 같은 task definition을 강제 재배포하면, 동시에 실행된 배포에서 다른 커밋의 이미지를 가져오거나 롤백 대상이 사라질 수 있습니다. 커밋 SHA 또는 ECR digest로 태그하고, 해당 URI를 반영한 task definition revision을 등록한 뒤 서비스에 지정하세요.

Also applies to: 39-56

🤖 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 at line 15, 배포 워크플로의 IMAGE_TAG를 latest 대신 커밋
SHA 또는 ECR digest 기반의 불변 값으로 변경하세요. 해당 이미지 URI를 사용해 새 task definition revision을
등록하고, 배포 서비스가 그 revision을 명시적으로 사용하도록 업데이트하세요.


jobs:
deploy:
name: Build and Deploy Auth
runs-on: ubuntu-latest
Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

GitHub 토큰 권한을 읽기 전용으로 제한하세요.

permissions가 없어 저장소/조직의 기본 GITHUB_TOKEN 권한에 의존합니다. 이 workflow에는 소스 checkout 권한만 필요하므로 최소 권한을 명시하세요.

수정 예시
 on:
   push:
     branches:
       - main
   workflow_dispatch:

+permissions:
+  contents: read
+
 env:
🤖 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 - 20, Update the deploy job
configuration near the jobs.deploy definition to explicitly set GITHUB_TOKEN
permissions to read-only for repository contents, while preserving the existing
build and deployment steps.

Source: Linters/SAST tools


steps:
- name: Checkout source code
uses: actions/checkout@v4

- 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
138 changes: 135 additions & 3 deletions API_SPEC.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,140 @@ servers:
- url: "Needs confirmation"
description: "Needs confirmation"
tags: []
paths: {}
paths:
/auth/oauth/naver:
get:
summary: "Redirect to Naver OAuth"
parameters:
- name: state
in: query
required: false
schema:
type: string
responses:
"302":
description: "Redirects to Naver authorization endpoint."
/auth/oauth/naver/callback:
get:
summary: "Handle Naver OAuth callback"
parameters:
- name: code
in: query
required: true
schema:
type: string
- name: state
in: query
required: false
schema:
type: string
responses:
"200":
description: "JWT tokens returned after Naver login."
"400":
$ref: "#/components/responses/BadRequestError"
"502":
$ref: "#/components/responses/NaverOauthError"
/auth/oauth/kakao:
get:
summary: "Redirect to Kakao OAuth"
parameters:
- name: state
in: query
required: false
schema:
type: string
responses:
"302":
description: "Redirects to Kakao authorization endpoint."
/auth/oauth/kakao/callback:
get:
summary: "Handle Kakao OAuth callback"
parameters:
- name: code
in: query
required: true
schema:
type: string
- name: state
in: query
required: false
schema:
type: string
responses:
"200":
description: "JWT tokens returned after Kakao login."
"400":
$ref: "#/components/responses/BadRequestError"
"502":
$ref: "#/components/responses/KakaoOauthError"
Comment on lines +20 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

OAuth 성공·리다이렉트 응답 계약을 명시하세요.

구현은 시작 엔드포인트에서 Location 헤더를 포함한 302를, 콜백에서 ApiResponse<OauthGoogleCallbackResponse> JSON을 반환하지만 명세에는 설명만 있습니다. 각 302의 Location 헤더와 각 200의 application/json 스키마·필드를 정의해야 클라이언트가 계약대로 연동할 수 있습니다.

As per coding guidelines, API_SPEC.yaml is the source of truth for paths, response fields, status codes, and error responses.

🤖 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 20 - 76, Update the OAuth start and callback
response definitions for the Naver and Kakao endpoints in API_SPEC.yaml: define
the 302 responses with a required Location header, and define each 200 response
as application/json using the
ApiResponse<OauthGoogleCallbackResponse>-equivalent schema with all returned
fields. Preserve the existing status codes and error response references.

Source: Coding guidelines

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 +79 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

오류 응답에 code 필드를 추가하세요.

ErrorResponsemessagedata만 정의합니다. 오류를 안정적으로 분기할 수 있도록 codemessage를 필수 필드로 정의하고, 모든 오류 예시 및 전역 예외 응답도 같은 형식으로 맞추세요.

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 79 - 90, Update the ErrorResponse schema to add a
required string code field alongside message, while retaining data as needed;
then align all error examples and global exception responses with the same
code-and-message format.

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
NaverOauthError:
description: "Naver OAuth request failed."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: Auth/Oauth/Naver] Naver Access Token을 발급받을 수 없습니다."
data: null
KakaoOauthError:
description: "Kakao OAuth request failed."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: Auth/Oauth/Kakao] Kakao Access Token을 발급받을 수 없습니다."
data: null
InternalServerError:
description: "Unexpected server error."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
message: "[ERROR: ?/?] 서버 내부 오류가 발생했습니다."
data: null
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`.
Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

서비스 개요의 나머지 오래된 설명도 함께 갱신하세요.

같은 문서가 controller, service, DTO, HTTP API, 인증 동작이 없다고 설명해 현재 OAuth 구현과 모순됩니다. 패키지 구조·주요 API·OAuth 인증·테스트 현황 섹션도 현재 상태에 맞추세요.

🤖 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 `@docs/service-overview.md` around lines 46 - 47, 서비스 개요 문서의 오래된 설명을 현재 구현에 맞게
갱신하세요. `com.example.auth.global.exception`, `AuthException`,
`GlobalExceptionHandler`를 포함한 패키지 구조를 반영하고, controller·service·DTO 구성, 주요 HTTP
API, OAuth 인증 동작, 테스트 현황을 실제 코드와 일치하도록 수정하세요.


## 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`
Comment on lines +40 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Naver·Kakao OAuth 오류 상태 코드를 명시적으로 정렬하세요.

API_SPEC.yaml은 Naver/Kakao OAuth 실패를 각각 502로 계약하지만, 이 문서는 두 예외를 “Other AuthException”으로 묶어 500으로 설명합니다. NaverOauthExceptionKakaoOauthException의 실제 전역 처리기 매핑을 별도로 문서화하고 API 명세와 일치시키세요.

As per coding guidelines, API_SPEC.yaml is the source of truth for the service API contract.

🤖 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 `@docs/service-policy.md` around lines 40 - 41, Update the authentication error
mapping documentation to list NaverOauthException and KakaoOauthException
separately with 502 Bad Gateway, matching their global handler behavior and the
API_SPEC.yaml contract. Keep unrelated AuthException cases under the 500
Internal Server Error entry.

Source: Coding guidelines

- Other `Exception`: `500 Internal Server Error`

## API Behavior Policy

Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/example/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ public ResponseEntity<ApiResponse<OauthGoogleCallbackResponse>> googleOauthCallb
return ResponseEntity.ok(ResponseUtil.success("Google 로그인에 성공했습니다.", response));
}

@GetMapping("/oauth/naver")
public ResponseEntity<Void> naverOauth(@RequestParam(required = false) String state) {
URI redirectUri = authService.createNaverAuthorizationUri(state);
return ResponseEntity.status(HttpStatus.FOUND)
.location(redirectUri)
.build();
}

@GetMapping("/oauth/naver/callback")
public ResponseEntity<ApiResponse<OauthGoogleCallbackResponse>> naverOauthCallback(
@RequestParam String code,
@RequestParam(required = false) String state
) {
OauthGoogleCallbackResponse response =
authService.loginWithNaver(code, state);
return ResponseEntity.ok(ResponseUtil.success("Naver 로그인에 성공했습니다.", response));
}
Comment on lines +60 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

OAuth state를 서버에서 생성하고 콜백에서 검증하세요.

현재 state는 요청값을 provider에 전달한 뒤 그대로 콜백에서 다시 받기만 하므로, 로그인 요청과 콜백을 사용자 브라우저에 바인딩하지 않습니다. 공격자가 자신의 authorization code를 피해자 콜백에 유도하는 login CSRF를 막을 수 없습니다. 예측 불가능한 state를 서버 측 저장소 또는 짧은 수명의 서명된 SameSite 쿠키에 바인딩하고, 콜백에서 일회성으로 검증·소비하세요.

Also applies to: 78-94

🤖 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/controller/AuthController.java` around lines
60 - 76, Update AuthController.naverOauth to generate an unpredictable
server-side state, bind it to the user session or a short-lived signed SameSite
cookie, and pass that generated value to authService.createNaverAuthorizationUri
instead of trusting the request parameter. In naverOauthCallback, validate and
consume the stored state exactly once before calling authService.loginWithNaver,
rejecting missing, mismatched, expired, or replayed values.


@GetMapping("/oauth/kakao")
public ResponseEntity<Void> kakaoOauth(@RequestParam(required = false) String state) {
URI redirectUri = authService.createKakaoAuthorizationUri(state);
return ResponseEntity.status(HttpStatus.FOUND)
.location(redirectUri)
.build();
}

@GetMapping("/oauth/kakao/callback")
public ResponseEntity<ApiResponse<OauthGoogleCallbackResponse>> kakaoOauthCallback(
@RequestParam String code,
@RequestParam(required = false) String state
) {
OauthGoogleCallbackResponse response =
authService.loginWithKakao(code, state);
return ResponseEntity.ok(ResponseUtil.success("Kakao 로그인에 성공했습니다.", response));
}

@PostMapping("/refresh")
public ResponseEntity<ApiResponse<RefreshResponse>> refresh(@RequestBody RefreshRequest request) {
// Refresh Token이 유효하면 새로운 토큰 묶음을 발급합니다.
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/com/example/auth/global/client/KakaoOauthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.example.auth.global.client;

import com.example.auth.global.client.dto.response.KakaoTokenResponse;
import com.example.auth.global.client.dto.response.KakaoUserInfoResponse;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;

@Component
public class KakaoOauthClient {

private final RestClient restClient;
private final String clientId;
private final String clientSecret;
private final String redirectUri;
private final String authorizationUri;
private final String tokenUri;
private final String userInfoUri;
private final String scope;

public KakaoOauthClient(
@Value("${oauth.kakao.client-id}") String clientId,
@Value("${oauth.kakao.client-secret}") String clientSecret,
@Value("${oauth.kakao.redirect-uri}") String redirectUri,
@Value("${oauth.kakao.authorization-uri}") String authorizationUri,
@Value("${oauth.kakao.token-uri}") String tokenUri,
@Value("${oauth.kakao.user-info-uri}") String userInfoUri,
@Value("${oauth.kakao.scope}") String scope
) {
this.restClient = RestClient.create();
this.clientId = clientId;
this.clientSecret = clientSecret;
this.redirectUri = redirectUri;
this.authorizationUri = authorizationUri;
this.tokenUri = tokenUri;
this.userInfoUri = userInfoUri;
this.scope = scope;
}

public URI createAuthorizationUri(String state) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(authorizationUri)
.queryParam("response_type", "code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri);

if (StringUtils.hasText(scope)) {
builder.queryParam("scope", scope);
}

if (StringUtils.hasText(state)) {
builder.queryParam("state", state);
}

return builder.build()
.encode()
.toUri();
}

public KakaoTokenResponse requestToken(String code) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", redirectUri);
body.add("code", code);

if (StringUtils.hasText(clientSecret)) {
body.add("client_secret", clientSecret);
}

return restClient.post()
.uri(tokenUri)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(KakaoTokenResponse.class);
}

public KakaoUserInfoResponse requestUserInfo(String accessToken) {
return restClient.get()
.uri(userInfoUri)
.headers(headers -> headers.setBearerAuth(accessToken))
.retrieve()
.body(KakaoUserInfoResponse.class);
Comment on lines +76 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate target file =="
fd -a 'KakaoOauthClient.java|GlobalExceptionHandler.java|application.*\.(yml|yaml|properties)' . || true

echo "== target file outline =="
ast-grep outline src/main/java/com/example/auth/global/client/KakaoOauthClient.java --view expanded || true

echo "== target file relevant lines =="
cat -n src/main/java/com/example/auth/global/client/KakaoOauthClient.java | sed -n '1,140p'

echo "== search exception classes and global handler =="
rg -n "class KakaoOauthException|KakaoOauthException|RestClientException|GlobalExceptionHandler|ResponseEntity|\n.*Exception" src/main/java -S || true

echo "== search RestClient setup =="
rg -n "RequestHeaderExchangeFilterFunction|DefaultClientRequestAuthenticator|KakaoOauthClient|restClient|`@Component`|`@Service`" src/main/java/com/example -S || true

Repository: MomentLit/Auth

Length of output: 8229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== global exception handler =="
cat -n src/main/java/com/example/auth/global/exception/GlobalExceptionHandler.java | sed -n '1,180p'

echo "== exception-related files =="
fd -a '.*(Exception|Error).*\.java$' src/main/java || true
for f in $(fd '.*(Exception|Error).*\.java$' src/main/java || true); do
  echo "---- $f ----"
  cat -n "$f" | sed -n '1,140p'
done

echo "== auth service relevant lines =="
cat -n src/main/java/com/example/auth/service/AuthService.java | sed -n '1,180p'

echo "== build files / dependencies =="
fd 'build.gradle|settings.gradle|pom.xml' . -x sh -c 'echo "---- $1 ----"; sed -n "1,220p" "$1"' sh {}

Repository: MomentLit/Auth

Length of output: 25098


🌐 Web query:

Spring Framework RestClient retrieve HTTP response status error exceptions RestClientException

💡 Result:

In the Spring Framework, the RestClient class handles HTTP error responses (status codes 4xx and 5xx) by throwing a RestClientException or one of its subclasses by default [1][2][3]. Specifically, if no custom status handler is defined, the client throws a RestClientResponseException when receiving a response with a status code of 400 or higher [4][5]. To customize this behavior, you can use the onStatus method on the ResponseSpec interface to define specific handlers for different HTTP status codes [1][6]. Key points for handling status errors: Default Behavior: By default, RestClient raises a RestClientException (specifically RestClientResponseException) for 4xx and 5xx status codes [2][5][7]. Customizing Handlers: You can override the default behavior by registering a status handler using the.onStatus(Predicate, ErrorHandler) method [4][8]. Predicate and Handler: The onStatus method accepts a predicate to match the status code and a handler function that defines how to process the request and response, often by throwing a custom exception [1][6]. Example of custom error handling: restClient.get.uri("https://example.com/api/resource").retrieve.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { throw new MyCustomException("Client error occurred: " + response.getStatusCode); }).body(String.class); Global Configuration: If you want to apply custom error handling to all requests, you can configure it at the builder level using.defaultStatusHandler(HttpStatusCode::isError, (request, response) ->...) when creating the RestClient instance [1][2]. RestClientResponseException provides access to the raw status code, headers, and the response body, allowing you to extract detailed information if you choose to catch the default exception rather than defining a custom handler [7].

Citations:


Kakao OAuth 콜백 시 Kakao API 실패를 502로 반환하도록 예외를 변환하세요.

RestClient.retrieve()는 Kakao API가 4xx/5xx를 반환하면 RestClientException(서버는 RestClientResponseException)을 던지므로, loginWithKakao()의 access token null 검증은 실행되지 않습니다. 이 예외가 전역 Exceptionhandler로 내려가 500이 되는데,requestToken()/requestUserInfo()호출에서RestClientException을 잡아 KakaoOauthException`으로 재投递하면 기존 Kakao 전용 handler가 502 Bad Gateway를 반환합니다.

🤖 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/client/KakaoOauthClient.java` around
lines 76 - 89, Update KakaoOauthClient methods requestToken() and
requestUserInfo() to catch RestClientException, including
RestClientResponseException, from the Kakao API calls and rethrow
KakaoOauthException while preserving the original cause. Ensure loginWithKakao()
receives the translated exception so the existing Kakao-specific handler returns
502 instead of allowing the error to reach the generic 500 handler.

}
}
Loading