diff --git a/build.gradle b/build.gradle
index 9305e703fb238deb9b38371a98864482965a4253..5cef9f6b166caa00c1f2ad460522a592a6bd8e02 100644
--- a/build.gradle
+++ b/build.gradle
@@ -28,6 +28,8 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-web'
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
+    runtimeOnly 'com.mysql:mysql-connector-j'
+    implementation 'org.pacesys:openstack4j:3.1.0'
     annotationProcessor 'org.projectlombok:lombok'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..9eb992b64dc063a267fd03a19d9d071e56ce43b1
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -0,0 +1,19 @@
+package com.aolda.itda.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+    @Override
+    public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "FETCH")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin")
+        ;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/controller/AuthController.java b/src/main/java/com/aolda/itda/controller/AuthController.java
new file mode 100644
index 0000000000000000000000000000000000000000..274ad8fe399c07e460c495151160a017cd8f1b52
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/AuthController.java
@@ -0,0 +1,27 @@
+package com.aolda.itda.controller;
+
+import com.aolda.itda.dto.auth.LoginRequestDTO;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+
+    @PostMapping("/login")
+    public ResponseEntity<Object> login(HttpServletResponse response,
+                                        @RequestBody LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
+
+        return ResponseEntity.ok(authService.userLogin(response, loginRequestDTO));
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..b91b3af32112b005db11a228e682e42889db7505
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
@@ -0,0 +1,11 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class LoginRequestDTO {
+    private String id;
+    private String password;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5aa14a6aae01ecd12a55f25c5ac966266ea8411
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class LoginResponseDTO {
+    private Boolean isAdmin;
+    private List<ProjectIdAndNameDTO> lists;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ca894d2bf9b9cd5f29244ac95e7c4950daec174
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
@@ -0,0 +1,16 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ProjectIdAndNameDTO {
+
+    private String id;
+    private String name;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3b9b8ecdc53343703a4f8b8cebbe6bbd6de4228
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ProjectRoleDTO {
+    private String projectName;
+    private String roleName;
+}
diff --git a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d1f3600c420d9d31b17e2b3faa8b399a6d5043f
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class ApiExceptionHandler {
+
+    @ExceptionHandler(value = CustomException.class)
+    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
+        log.error("[handleCustomException] {} : {}", e.getErrorCode().name(), e.getErrorCode().getMessage());
+        return ErrorResponse.fromException(e);
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/CustomException.java b/src/main/java/com/aolda/itda/exception/CustomException.java
new file mode 100644
index 0000000000000000000000000000000000000000..d110063b5baf441b8dfba80e64ef0eb21816cb09
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/CustomException.java
@@ -0,0 +1,19 @@
+package com.aolda.itda.exception;
+
+import lombok.Getter;
+
+@Getter
+public class CustomException extends RuntimeException{
+    private ErrorCode errorCode;
+
+    private String info;
+
+    public CustomException(ErrorCode errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    public CustomException(ErrorCode errorCode, String info){
+        this.errorCode = errorCode;
+        this.info = info;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bcb6431ef8ff826b318abd9d72cc5d2255aadcd
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -0,0 +1,16 @@
+package com.aolda.itda.exception;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum ErrorCode {
+
+    // User
+    INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다");
+
+    private final HttpStatus status;
+    private final String message;
+}
diff --git a/src/main/java/com/aolda/itda/exception/ErrorResponse.java b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bec06031ebb486ecc4b54ea218380e05bbc6681
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
@@ -0,0 +1,29 @@
+package com.aolda.itda.exception;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@Getter
+@Builder
+public class ErrorResponse {
+
+    private final HttpStatus status;  // HTTP 상태 코드
+    private final String code;        // 에러 코드
+    private final String message;     // 에러 메시지
+
+    public static ResponseEntity<ErrorResponse> fromException(CustomException e) {
+        String message = e.getErrorCode().getMessage();
+        if (e.getInfo() != null) {
+            message += " " + e.getInfo(); // 추가 정보가 있는 경우 결합
+        }
+        return ResponseEntity
+                .status(e.getErrorCode().getStatus())
+                .body(ErrorResponse.builder()
+                        .status(e.getErrorCode().getStatus())
+                        .code(e.getErrorCode().name())
+                        .message(message)
+                        .build());
+    }
+}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f071eef2e680b74779350994fb6c355e2333b66
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -0,0 +1,159 @@
+package com.aolda.itda.service;
+
+import com.aolda.itda.dto.auth.LoginRequestDTO;
+import com.aolda.itda.dto.auth.LoginResponseDTO;
+import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
+import com.aolda.itda.dto.auth.ProjectRoleDTO;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.*;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    @Value("${spring.server.keystone}")
+    private String keystone;
+    @Value("${spring.server.admin-id}")
+    private String adminId;
+    @Value("${spring.server.admin-password}")
+    private String adminPassword;
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 사용자 로그인 후 토큰 발행 및 Role 반환
+    public LoginResponseDTO userLogin(HttpServletResponse response, LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
+        Map<String, String> user = getToken(loginRequestDTO.getId(), loginRequestDTO.getPassword());
+
+        String userId = user.get("id");
+        String token = user.get("token");
+
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        response.addHeader("X-Subject-Token", token);
+        return LoginResponseDTO.builder()
+                .isAdmin(false)
+                .lists(getProjectsWithUser(user))
+                .build();
+    }
+
+    // 특정 사용자의 토큰 발행
+    private Map<String, String> getToken(String id, String password) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"password\"\n" +
+                "            ],\n" +
+                "            \"password\": {\n" +
+                "                \"user\": {\n" +
+                "                    \"name\": \""+ id + "\",\n" +
+                "                    \"domain\": {\n" +
+                "                        \"name\": \"Default\"\n" +
+                "                    },\n" +
+                "                    \"password\": \"" + password + "\"\n" +
+                "                }\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
+        ResponseEntity<Map> res = restTemplate.postForEntity(url, requestEntity, Map.class);
+
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return Map.of("id", userId,
+                    "token", token);
+    }
+
+    // 특정 사용자의 프로젝트별 Role 반환
+    private List<ProjectRoleDTO> getRolesWithProjects(Map<String, String> user) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", getAdminToken());
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("role_assignments");
+
+        List<ProjectRoleDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+
+            String projectName = assignment.path("scope").path("project").path("name").asText();
+            String roleName = assignment.path("role").path("name").asText();
+
+            ProjectRoleDTO projectRoleDTO = new ProjectRoleDTO(projectName, roleName);
+            lists.add(projectRoleDTO);
+
+        }
+
+        return lists;
+    }
+
+    // 관리자용 토큰 발행
+    public String getAdminToken() {
+        Map<String, String> user = getToken(adminId, adminPassword);
+        return user.get("token");
+    }
+
+    private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/users/" + userId + "/projects";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", getAdminToken());
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("projects");
+
+        List<ProjectIdAndNameDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+            String projectId = assignment.path("id").asText();
+            String projectName = assignment.path("name").asText();
+            lists.add(new ProjectIdAndNameDTO(projectId, projectName));
+        }
+        return lists;
+    }
+}