OAuth란 무엇인가?
OAuth(Open Authentication)은 개방형 인증 표준으로, 사용자가 자신의 자격 증명(아이디, 비밀번호)을 직접 공유하지 않고도 한 서비스(클라이언트)가 다른 서비스(리소스 서버, ex 카카오, 네이버 등)에 있는 사용자 정보에 접근할 수 있도록 권한을 부여하는 메커니즘이다.
OAuth의 동작 원리
인증(Authentication)과는 다르게 사용자의 신원을 직접 확인하는 것이 아니라, 특정 리소스에 접근할 수 있는 권한 토큰을 발행하여 사용자를 대신해 리소스에 접근하게 한다(액세스 할 수 있는 권한을 부여하는 것).
OAuth의 용어
- 액세스 토큰(Access Token): 클라이언트가 리소스 서버의 리소스에 접근하기 위한 권한을 부여받는 토큰. 액세스 토큰은 권한 서버로부터 발급되며, 일반적으로 제한된 유효 기간을 가지고 있다.
- 리프레시 토큰(Refresh Token): 리프레시 토큰은 액세스 토큰의 유효 기간이 만료된 후 새로운 액세스 토큰을 받기 위한 토큰. 이를 통해 사용자는 다시 로그인할 필요 없이 토큰 유효 시간 갱신만으로 계속 애플리케이션을 사용할 수 있다.
- 범위(Scope): 범위는 클라이언트가 리소스에 대한 어떤 작업을 수행할 수 있는지를 정의하는 문자열. 범위는 권한 서버에 의해 정의되며, 클라이언트는 특정 범위의 액세스 권한을 요청할 수 있다.
- 인증 코드(Authorization Code): 인증 코드는 클라이언트가 액세스 토큰을 얻기 위한 중간 단계로 사용되는 코드. 인증 코드 부여(Authorization Code Grant) 방식을 통해 권한 서버로부터 발급되며, 이를 사용하여 액세스 토큰과 리프레시 토큰을 얻을 수 있다.
OAuth의 역할
- 클라이언트(Client): 사용자의 리소스에 접근하고자 하는 애플리케이션 ex. 인프런
- 리소스 소유자(Resource Owner): 리소스의 실제 소유자 ex. 인프런에 로그인하는 사용자
- 리소스 서버(Resource Server): 보호된 사용자 리소스(예: 이름, 성별, 생년월일 등)를 호스팅하는 서버 리소스를 호스팅하는 서버로, 액세스를 허용하거나 거부한다. OAuth 2.0 토큰을 사용하여 클라이언트에게 리소스에 액세스할 권한을 부여하고 실제 데이터를 제공한다. ex. 구글 서버 (사용자의 Gmail 주소, 프로필 사진, 이름 등의 정보를 가지고 있는 서버)
- 권한 서버(Authorization Server): 클라이언트에게 접근 토큰을 발급하는 서버. 리소스 소유자의 인증을 처리하고, 리소스 소유자의 동의를 받아 클라이언트에게 접근 권한을 부여한다. ex. 구글의 OAuth 인증 서버
리소스 서버와 권한 서버는 어떻게 다른가?
- 권한 서버 (Authorization Server)의 역할
- 요청 스코프 검증: 클라이언트가 접근 토큰 발급을 요청할 때 함께 전달하는 스코프(프로필 사진, 이름과 같은 정보들에 대한 권한) 파라미터가 유효한지 확인한다. 이는 권한 서버에 미리 등록된 유효한 스코프 목록에 포함되는지 여부를 검사하는 것을 의미한다. 정의되지 않거나 유효하지 않은 스코프는 거부한다.
- 클라이언트 및 사용자 동의 기반 스코프 할당: 클라이언트가 요청한 스코프가 해당 클라이언트의 등록 정보(웹 또는 앱에서 요구하는 권한)에 허용된 스코프 범위 내에 있는지 확인한다. 또한, 리소스 소유자(사용자)가 클라이언트가 요청한 스코프에 대해 실제로 동의했는지 확인하고, 사용자가 동의한 스코프만을 최종적으로 접근 토큰에 포함하여 발급한다. 사용자가 특정 스코프에 동의하지 않았다면 해당 스코프는 접근 토큰에서 제외된다(알람을 받으시겠습니까? > 아니오).
- 접근 토큰에 스코프 포함: 최종적으로 사용자가 동의한 유효한 스코프 정보를 접근 토큰에 포함하여 클라이언트에게 발급한다. 이 스코프 정보는 보통 JWT(JSON Web Token)에 포함된다(토큰 그 자체에 필요한 모든 정보가 암호화된 형태로 담김).
- 권한 서버는 접근 토큰을 발급하는 시점에 스코프의 유효성을 검증한다.
- 리소스 서버 (Resource Server)의 역할
- 접근 토큰 유효성 검증: 클라이언트가 요청 헤더에 포함하여 보낸 접근 토큰이 유효한지 확인한다. 이는 토큰의 서명, 만료 시간 등을 검증하는 과정이다.
- 토큰 내 스코프 추출: 유효성이 검증된 접근 토큰으로부터 스코프 정보를 추출한다.
- 요청 리소스에 대한 스코프 검증: 클라이언트가 요청한 특정 리소스(API 엔드포인트)에 접근하기 위해 필요한 스코프와 접근 토큰에 포함된 실제 스코프를 비교한다. 만약 접근 토큰의 스코프가 해당 리소스에 접근하기 위해 필요한 스코프를 포함하지 않는다면, 리소스 서버는 요청을 거부하고 403 Forbidden과 같은 응답을 반환한다. 예를 들어, read_profile 스코프가 필요한 API에 write_photos 스코프만 있는 토큰으로 요청이 오면 거부한다.
- 리소스 서버는 클라이언트로부터 보호된 리소스에 대한 요청을 받을 때 스코프의 유효성을 검증한다.
스코프 정보 동기화
실제 서비스에서는 리소스 서버와 권한 서버가 분리되어 있을 수도, 하나의 서버에서 동작할 수도 있다(구글, 네이버 등은 보통 분리함)
권한 서버와 리소스 서버가 분리되어 있을 때 스코프 정보를 동기화하는 가장 일반적이고 효율적인 방법은 접근 토큰 자체에 스코프 정보를 포함시키는 것이다.
JWT (JSON Web Token) 사용:
- 접근 토큰에 포함: 권한 서버는 JWT 형태로 접근 토큰을 발급할 때, 사용자가 승인한 스코프를 JWT의 페이로드(payload) 내 scope 클레임(claim)에 직접 포함시킨다.
- 리소스 서버의 독립성: 리소스 서버는 이 JWT를 수신하면, 권한 서버에 별도로 질의할 필요 없이 자체적으로 JWT의 서명을 검증하고 페이로드에서 스코프 정보를 추출하여 유효성 검사를 수행할 수 있다. 이는 리소스 서버가 권한 서버에 대한 의존성을 줄이고, 분산 환경에서 확장성을 높이는 데 기여한다.
- 키 공유: 이를 위해 권한 서버와 리소스 서버는 JWT의 서명을 검증할 수 있는 공개 키(Public Key)를 공유해야 한다 (비대칭 암호화 방식 사용 시). 권한 서버는 비공개 키로 토큰에 서명하고, 리소스 서버는 공개 키로 서명을 검증한다.
JWT 토큰은 뭐고, 왜 사용해야 할까?
https://velog.io/@chuu1019/알고-쓰자-JWTJson-Web-Token
OAuth 권한 부여 방식
- 인증 코드 요청
- 클라이언트가 사전에 권한 서버로부터 발급한 클라이언트 ID, redirect_uri 정보와 함께 response_type을 code로 지정하여 요청한다.
- 로그인
- 권한 서버에서 로그인 페이지를 제공하고 리소스 소유자가 로그인 한다.
- 인증 코드 전달
- 로그인에 성공하면 권한 서버는 전달받은 redirect_uri로 인증 코드를 전달한다.
- 엑세스 토큰 발급
- 클라이언트가 권한 부여 승인 코드를 통해 엑세스 토큰 (및 리프레시 토큰)을 발급받는다.
- 이때 사전에 권한 서버로부터 발급한 클라이언트 ID와 클라이언트 시크릿 (Client Secret) 정보가 필요하다.
자체 로그인 방식과 소셜 로그인 방식이 함께 사용되는 경우 DB는 어떻게 구성해야 할까?
고려해야 하는 사항은 다음과 같았다.
- 데이터베이스(DB)는 사용자 정보를 중복 없이 통합 관리되어야 함
- 각 인증 방식에 필요한 데이터를 효율적으로 저장하도록 구성해야 함
- 각 인증 방식(구글, 애플 등)에서 얻어올 수 있는 데이터를 분석하고, 사용자별 동일한 정보를 저장해야 함
3번 같은 경우, 구글에서는 이메일, 이름, 프로필 사진 정보만 받아올 수 있었고, 성별, 생일 등 민감 정보는 2024년 이후 구글 정책상 소셜 로그인에서 제공하지 않는다고 한다.
프로필 정보, 전화번호 등과 같이 많은 정보를 받아올 수 있는 네이버, 카카오와 구글 로그인 방식을 함께 채택하는 경우 이 데이터들의 간극을 고려하여야 했다.
그래서 소셜 로그인에서 제공하지 않는 정보(ex. 전화번호, 주소 등)는 최초 가입 시 추가 입력을 요구하거나, nullable로 관리하는 프로세스를 채택하였다.
최종 설계된 user 관련 table은 다음과 같았다.
CREATE TABLE users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100),
profile_image_url VARCHAR(2048),
is_active BOOLEAN DEFAULT TRUE NOT NULL, -- 계정의 활성화 상태
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at TIMESTAMP WITH TIME ZONE NULL -- 소프트 삭제 시점
);
CREATE TABLE local_auth (
local_auth_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE NOT NULL, -- users.user_id 와 일대일 관계
password_hash VARCHAR(255) NOT NULL,
salt VARCHAR(64), -- 해시 함수가 솔트를 자체 포함하지 않을 경우
email_verified_at TIMESTAMP WITH TIME ZONE NULL, -- 이메일 인증 완료 시점
password_changed_at TIMESTAMP WITH TIME ZONE NULL, -- 비밀번호 마지막 변경 시점
reset_token VARCHAR(255) NULL, -- 비밀번호 재설정 임시 토큰
reset_token_expires_at TIMESTAMP WITH TIME ZONE NULL, -- reset_token 만료 시점
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
CREATE TABLE social_accounts (
social_account_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL, -- users.user_id 와 다대일 관계 (한 사용자 여러 소셜 계정)
provider VARCHAR(50) NOT NULL, -- 예: 'google', 'kakao', 'naver'
provider_user_id VARCHAR(255) NOT NULL, -- 각 소셜 서비스의 사용자 고유 ID
provider_email VARCHAR(255), -- 소셜 계정의 이메일 (계정 통합 시 활용)
access_token TEXT, -- 암호화하여 저장
refresh_token TEXT, -- 암호화하여 저장
token_expires_at TIMESTAMP WITH TIME ZONE NULL, -- 접근 토큰 만료 시점
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
-- provider와 provider_user_id 조합은 유니크해야 함 (복합 유니크 인덱스)
CONSTRAINT uq_provider_user_id UNIQUE (provider, provider_user_id),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
'학습 > CS' 카테고리의 다른 글
빅데이터(Big Data)에 대해서 (1) | 2024.07.20 |
---|---|
인증과 인가, 어떻게 다를까? (0) | 2024.07.19 |