때는 IrisCTF에 참여했을 때, OIDC 관련 문제가 나왔다.
사실 문제 자체는 그렇게 어렵지 않았지만, OIDC의 매커니즘 자체를 모르고 있었다보니 푸는 데 한 세월이 걸렸다.
요즘 OAuth, OIDC도 많이 사용하겠다, 이번 포스트는 이에 관한 내용이다.
이 포스트는 IrisCTF 2025에 출제된 bad-todo 문제를 기반으로 작성되었습니다.
OIDC란?
OpenID란, 비영리재단인 OpenID에서 관리하는 개방형 분산 인증 프로토콜이다. 그 중에서 2014년도에 출시된 3세대 OpenID 기술이 바로 OpenID Connect인데, OIDC는 이 OpenID Connect를 말한다.
OpenID Connect는 OAuth 2.0의 위에서 돌아가는 간단한 ID 계층이다. 이를 통해 사용자는 Authorization 서버에서 인증한 정보를 바탕으로 어플리케이션에서 본인의 신원을 입증할 수 있고, 사용자 정보를 안전하게 어플리케이션에 전달할 수 있다.
조금 쉽게 설명하자면..
이전에 OpenID, OAuth에 대해 들어본 적이 없었다면, 위 설명을 읽고도 이해가 잘 되지 않을 것이다. 하지만 아래 그림을 보면 바로 이해가 되지 않을까 싶다.

어떤 서비스를 이용할 때 구글 계정, 카카오 계정, 또는 기타 다른 서비스 계정으로의 가입 및 이용을 지원하는 경우가 있다. 이렇게 신뢰할 만한 타 서비스(대표적으로 구글)에 사용자 인증 절차를 위탁하여 하나의 인증 정보로 다양한 서비스에서 사용자 인증(Authentication)을 수행할 수 있도록 하는 것이 OpenID Connect다.
OIDC(OAuth 2.0)의 절차
일반적으로 OpenID Connect(또는 OAuth 2.0)는 다음과 같은 절차로 이루어진다.

먼저 사용자가 웹 애플리케이션에 접속하여 로그인 요청을 하면 웹 애플리케이션은 사용자를 인증 서버로 redirect한다. 사용자가 인증 서버에서 로그인을 수행하면 인증 서버는 회원정보를 확인하고, 인증에 성공하면 인증 서버는 승인 코드와 함께 사용자를 redirect 페이지로 이동시킨다.
그리고 클라이언트(웹 애플리케이션)에서 인증 서버로 승인 코드와 함께 요청을 하면, 인증 서버는 클라이언트에 토큰을 발급해준다. 이때 access token만을 발급해주는 것이 OAuth 2.0, 사용자 식별 정보가 포함된 id token도 발급해주는 것이 OpenID Connect다.
참고 - 일반적인 상황에서 클라이언트라고 하면 사용자를 가리키지만, 여기서는 인증을 위해 인증 서버에 요청을 하는 웹 서비스가 클라이언트가 된다. 이후 언급하는 클라이언트는 전부 웹 서비스를 의미한다.
실제 OIDC 작동 예시
실제로 OpenID Connect가 어떤 방식으로 작동하는지 확인해보자.
https://github.com/IrisSec/IrisCTF-2025-Challenges/tree/main/bad-todo
환경 세팅

문제의 첫 화면은 이런 식이다. 우리의 목적이 문제를 푸는 것은 아니니까 간단하게만 설명하면, 어떤 서비스로 로그인을 할 지 설정하는 과정이라 보면 된다. 구글 로그인을 사용한다면 구글 인증 서버의 주소가 들어가는 식이다. Client ID는 인증 서버에서 클라이언트(애플리케이션)를 식별하기 위한 고유값이다.
원래대로라면 인증 서버에 클라이언트를 등록해야 OAuth 로그인이 가능하다. 클라이언트를 신뢰할 수 있는 서비스로 만드는 것이다. 만약 클라이언트가 인증 서버가 신뢰할 수 없는 애플리케이션이라면 문제가 발생할 수도 있기 때문이다. (예를 들면, 피싱 페이지에서 구글 로그인을 유도해 탈취한 승인 코드로 토큰을 발급받는다던가, redirect uri를 조작하여 다른 페이지로 보내버리는 등의 악성 요청들)
그래서 OAuth 로그인을 지원하려면, 인증 서버에 미리 요청하여 Client ID와 secret값을 발급받고, 로그인 성공 시 redirect할 클라이언트 서비스 uri를 인증 서버에 미리 등록하는 과정을 거쳐야 한다.
그런데 ctf 문제 내겠다고 인증 서버에 미리 등록을 시켰을 리가 없다. (그리고 ctf 문제를 인증 서버에서 등록시켜줬을 리가 없다.) 그래서 Auth0이라는 서비스를 이용할 것이다.
Auth0은 웹 및 모바일 애플리케이션에서 인증 및 권한 부여 프로세스의 구현을 단순화하는 IDaaS(Identity-as-a-Service) 플랫폼이다. 쉽게 말하면, 굳이 OAuth(OIDC) 인증 과정을 구현하지 않고 이를 대신해주는 서비스다. 우리의 인증 서버는 이 Auth0이 맡게 될 것이다.

Auth0을 사용하더라도 등록과정이 간소화되었을 뿐, 등록은 해야 한다. Client ID를 받고, redirect URI를 Auth0 쪽에 등록해두면 끝이다.

로그인 요청

app.post("/start", asyncHandler(async (req, res) => {
let response = null;
try {
response = await safeJson(req.body.issuer + "/.well-known/openid-configuration");
} catch(e) {
res.sendStatus(400);
res.write("Invalid OpenID configuration ;_;");
res.end();
return;
}
...
위 코드는 문제 페이지에서 로그인 버튼을 눌렀을 때(일반적인 애플리케이션에서 구글 로그인 등을 눌렀을 때)의 동작이다. 로그인 요청을 보내면 Issuer 주소의 /.well-known/openid-configuration이라는 api로 요청을 보내게 된다. 반환값은 아래와 같다.
{"issuer":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/","authorization_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/authorize","token_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/oauth/token","device_authorization_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/oauth/device/code","userinfo_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/userinfo","mfa_challenge_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/mfa/challenge","jwks_uri":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/.well-known/jwks.json","registration_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/oidc/register","revocation_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/oauth/revoke","scopes_supported":["openid","profile","offline_access","name","given_name","family_name","nickname","email","email_verified","picture","created_at","identities","phone","address"],"response_types_supported":["code","token","id_token","code token","code id_token","token id_token","code token id_token"],"code_challenge_methods_supported":["S256","plain"],"response_modes_supported":["query","fragment","form_post"],"subject_types_supported":["public"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","private_key_jwt"],"claims_supported":["aud","auth_time","created_at","email","email_verified","exp","family_name","given_name","iat","identities","iss","name","nickname","phone_number","picture","sub"],"request_uri_parameter_supported":false,"request_parameter_supported":false,"id_token_signing_alg_values_supported":["HS256","RS256","PS256"],"token_endpoint_auth_signing_alg_values_supported":["RS256","RS384","PS256"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"end_session_endpoint":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/oidc/logout"}
여기서 authorization_endpoint가 로그인 페이지다. 클라이언트는 사용자를 이 authorization_endpoint 주소로 필요 정보를 포함시킨 상태로 redirect 시킨다. 사용자에게 로그인을 시키는 것이라 보면 된다.
아래는 관련 코드다.
if (response && response.issuer && response.authorization_endpoint && response.token_endpoint && response.userinfo_endpoint) {
const session = await newSession(req.body.issuer, req.body.client_id);
console.log(session);
const search = new URLSearchParams();
search.append("client_id", req.body.client_id);
search.append("redirect_uri", process.env.BASE + "/auth_redirect");
search.append("scope", "openid");
search.append("response_type", "code");
search.append("state", session);
res.setHeader("Set-Cookie", `session=${session}; HttpOnly; Max-Age=3600; SameSite=Lax; Secure`);
res.setHeader("Location", `${response.authorization_endpoint}?${search.toString()}`)
res.sendStatus(302);
} else {
res.sendStatus(400);
res.write("Invalid OpenID configuration ;_;");
res.end();
}
}));

좌측의 요청 부분은 우리가 제3자 서비스 로그인을 누른 동작이라고 이해하면 된다. 그 이후에는 클라이언트가 /.well-known/openid-configuration에서 인증 페이지의 위치를 받아오고, 우측의 응답 부분처럼 인증 유형을 지정해 사용자를 인증 서버로 redirect 시킨다.

인증 서버에서 인증이 완료되면 인증 서버는 사용자에게 승인 코드를 발급해 사전에 등록된 redirect 페이지로 사용자를 redirect한다.
토큰 발급
지금까지의 과정을 생각해보면, 사용자는 제3자 서비스의 정보로 로그인을 하겠다고 웹 서비스에게 알렸고, 이 웹 서비스는 클라이언트로서 인증 서버에 사용자를 인증해달라 요청한 상황이다. 그리고 사용자는 인증 서버에서 인증을 마치고 승인 코드를 발급받았고, 클라이언트의 redirect 페이지에 이 승인 코드를 전달하였다.
여기서부터는 클라이언트의 동작이다.
app.get("/auth_redirect", asyncHandler(async (req, res) => {
if (!req.cookies.session) return res.end("No session");
if (req.cookies.session !== req.query.state) return res.end("Bad state");
if (req.query.error) {
return res.end("identity provider gave us an error.");
}
const sessionDetails = await lookupSession(req.cookies.session);
const response = await safeJson(sessionDetails.idpUrl + "/.well-known/openid-configuration");
if (!response.token_endpoint) return res.end("No token endpoint");
if (!response.userinfo_endpoint) return res.end("No user info endpoint");
const search = new URLSearchParams();
search.append("grant_type", "authorization_code");
search.append("code", req.query.code);
search.append("redirect_uri", process.env.BASE + "/auth_redirect");
search.append("client_id", sessionDetails.clientId);
const tokenResponse = await safeJson(response.token_endpoint, {
method: "POST",
body: search.toString(),
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
if (!tokenResponse || !tokenResponse.access_token || !tokenResponse.token_type) return res.end("Bad token response");
...
사용자로부터 승인 코드를 넘겨받은 클라이언트는 사용자가 인증 서버에서 인증을 마쳤다는 것을 확인하고, 이 승인 코드를 통해 인증 서버로부터 사용자의 정보를 가져올 수 있는 권한을 받아야 한다. (이를 Authorization, 인가받는다고 한다. OAuth 2.0의 Auth가 이 의미다.)
클라이언트는 다시 한번 /.well-known/openid-configuration으로부터 token을 발급받는 위치를 받아오고, 요청 방식과 승인 코드, client ID등을 이용해 인증 서버에 요청하여 access token을 받는다.

이렇게 클라이언트는 인증 서버로부터 access_token과 id_token을 발급받게 된다. 이 access_token은 리소스 서버에 존재하는 사용자의 자원에 접근하려 할 때 사용되는 토큰이다. OAuth 2.0은 이 access_token을 발급하는 것이 목적인 프로토콜이다.
반면 OIDC는 인증(Authentication)이 목적인 프로토콜이기 때문에 사용자 신원이 담겨 있는 id_token을 같이 반환한다. 위에서 발급한 id_token의 내용을 확인해보면 다음과 같다.
{
"iss":"https://dev-qjdhtj7bfak85uq0.us.auth0.com/",
"aud":"5COHGiX9lufprfqq0IxdfVP1zqT4xFhs",
"iat":1737627809,
"exp":1737663809,
"sub":"auth0|6779b6e3dcadab9f184eadd1",
"sid":"A0Aik1PcBbwqaq-_I6D8NTMRQfkPACWK"
}
iss는 발급자, aud는 client id, iat와 exp는 발급시간과 유효기간, sub는 고유 id이다. 여기서는 사용자 신원을 확인할 정보가 sub값밖에 없지만, 요청 시 scope를 어떻게 설정하느냐에 따라 포함되는 정보가 달라지기도 한다.

이렇게 Auth0의 회원정보를 이용하여 웹 서비스의 로그인에 성공하였다. id token에서 받아온 인증 정보가 같이 출력되고 있는 것을 확인할 수 있다.
참고자료
[1] OpenID 공식문서
https://openid.net/specs/openid-connect-core-1_0.html
[2] IrisCTF2025 출제 문제 bad-todo
https://github.com/IrisSec/IrisCTF-2025-Challenges/tree/main/bad-todo
[3] OpenID(OIDC) 개념과 동작원리