MSA 인증 서비스 Keycloak REST API, Naver 로그인

Keycloak REST API


공식문서에 보면 정말 다양한 API 들이 제공된다. 그중 여기서 다뤄볼 API 는 access_token 을 발급받는 것과, 사용자 생성, Role 매핑, 사용자 조회 API 이다.

그리고 잠깐 Identity Provider 를 사용해서 OIDC(Open Id Connect) 를 지원하지 않는 네이버 로그인을 설명하고자 한다.


Keycloak REST API


Access Token 발급 API

 curl http://localhost:8080/auth/realms/{keycloak realm}/protocol/openid-connect/token \
 -dclient_id={keycloak client id} \
 -dclient_secret={keycloak client secret} \
 -dscope=profile \
 -dgrant_type=client_credentials

위에서 따로 입력해야 하는 부분은 realm, clientid, client secret 정도이다. 물론 client_credentials 를 사용할 경우 clientid, client secret 이 필요하다. 결과는 다음과 같이 출력된다.

이제 받아온 access token 으로 Keycloak 인증을 하고 다양한 API 를 사용할 수 있다. 위에서 말한 몇가지를 사용한 예제를 보자.

Create User

curl http://localhost:8080/auth/admin/realms/{keycloak realm}/users \
-H "Authorization: bearer {access_token}" \
-X POST \
--data '{
    "username": "{username}",
    "enabled": "true",
    "credentials": [
        {
            "value": "{password}"
        }
    ]
}'

추가로 공식문서에 보면 realmRoles 라고 있는데, 작동이 안된다. 왠지는 모르겠다. 개발을 안한건가? 아무튼 Role 를 추가하고자 할때 조금 힘들지만 Keycloak 대시보드에서 Role 을 직접 추가해서 API 로 사용자와 Role 을 매핑시켜줘야 한다. Role 매핑 API 는 다음과 같다. 여기서는 Client Role 매핑을 사용했다. Realm Role 매핑과는 Path 하나의 차이만 있다.

Client Role Mapping

curl http://localhost:8080/auth/admin/realms/sometrend-realm/users/{keycloak user id}/role-mappings/clients/{keycloak client} \
-H "Authorization: bearer {access_token}" \
-X POST \
--data '[
    {
        "id": "{realm/client role id}",
        "name": "{role name}",
        "clientRole": "true",
        "containerId": "{realm/client id}"
    }
]'

Realm 매핑을 사용하려면 뒤에 clients/{keycloak client} 를 제거해주면 된다.

사용자를 추가하고 Role 매핑까지 해줬으니 이제 추가된 사용자를 조회해보자. 다음은 전체 사용자를 조회하는 요청이다. 만약 username 또는 Email 등으로 조건을 주고 조회를 하고싶으면 다양한 Parameter 가 공식문서에 나와있다.

Get User Parameter

Find User

curl http://localhost:8080/auth/admin/realms/{keycloak realm}/users



간단하게 API 를 이용해서 사용자를 Keycloak DB 에 추가했다. 이제 로그인도 잘된다. 여기서 추가로 소셜 로그인을 추가하고 싶다. 뭐 예를들어 구글이라던가 페이스북이라던가.. 다행이도 Keycloak 은 OIDC 를 지원하는 다양한 소셜 로그인들을 지원한다. 아래에 그림을 보면 웬만한 소셜 로그인은 지원한다고 볼 수 있다.

그런데 중요한 것은 여긴 한국이다. 한국은 네이버, 카카오 사용자 수를 무시할 수 없다. 그러기에 네이버 로그인, 카카오 로그인을 추가해줘야 한다. 근데 위 사진에서 보듯이 네이버랑 카카오는 눈을 씻고 찾아봐도 없다. 이유는 네이버와 카카오의 경우 OIDC 를 지원하지 않고 OAuth 만을 지원하기 때문이다. 그래서 이부분을 개발자가 직접 Identity Provider 를 구현해서 OAuth 만을 사용하는 소셜 로그인을 연결해줘야 한다.

네이버 용 Identity Provider 를 만들어보자. 아래 구현은 다음 내용을 참고했다.

참고: Baidu Identity Provider

먼저 프로젝트 구조는 다음과 같다. 이전에 User Storage SPI 를 구현할때 처럼 META-INF/services 아래 파일과 Provider, ProviderFactory 가 존재하는 것을 볼 수 있다. 여기는 외부 DB 와 연결해줘야하는 복잡한 구현은 없기때문에 파일도 몇개 안된다. 그러면 하나하나 보자.

먼저 META-INF/services 아래 파일들을 보자. UserStorageSPI 와 다른점이라면 Mapper 파일이 추가됬다. 일단 정확히 무슨역할을 하는지 모르니 Keycloak 클래스명과 패키지명으로 파일명을 작성하고 내용으로는 개발자가 만든 매퍼와 팩토리 클래스의 패키지명과 파일명을 적어주도록 하자.

Gradle Dependency

  compile 'org.keycloak:keycloak-services:10.0.2'
  compile 'org.keycloak:keycloak-server-spi:10.0.2'
  compile 'org.keycloak:keycloak-server-spi-private:10.0.2'

org.keycloak.broker.provider.IdentityProviderMapper

moe.saru.keycloak.modules.naver.NaverUserAttributeMapper

org.keycloak.broker.social.SocialIdentityProviderFactory

moe.saru.keycloak.modules.naver.NaverIdentityProviderFactory

NaverUserAttributeMapper

import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;

/**
 * User attribute mapper.
 */
public class NaverUserAttributeMapper extends AbstractJsonUserAttributeMapper {

	private static final String[] cp = new String[] { NaverIdentityProviderFactory.PROVIDER_ID };

	@Override
	public String[] getCompatibleProviders() {
		return cp;
	}

	@Override
	public String getId() {
		return "naver-user-attribute-mapper";
	}

}

정확히 매퍼가 하는 역할은 확실치 않다. 다만 예상컨데 사용자 속성 매퍼라는 점… 아마 Identity Provider 정보 입력 화면에서 보이는 속성들을 매핑시켜주는 것 같다.

NaverIdentityProviderFactory


import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;

public class NaverIdentityProviderFactory extends AbstractIdentityProviderFactory<NaverIdentityProvider> implements SocialIdentityProviderFactory<NaverIdentityProvider> {

    public static final String PROVIDER_ID = "naver";

    @Override
    public String getName() {
        return "naver";
    }

    @Override
    public NaverIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
        return new NaverIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public OAuth2IdentityProviderConfig createConfig() {
        return new OAuth2IdentityProviderConfig();
    }
}

친숙한 Factory 클래스다. 역시 Id 값을 넣어주며, Provider 를 생성해준다. 주의할 점은 아래 createConfig 가 없으면 에러가 난다는 점이다. 반드시 작성해주자 여기서는 OAuth2 IdentityProvider 설정을 생성하였다.

NaverIdentityProvider

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import java.util.Iterator;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;

public class NaverIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {

	public static final String AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
	public static final String TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
	public static final String PROFILE_URL = "https://openapi.naver.com/v1/nid/me";
	public static final String DEFAULT_SCOPE = "basic";

	public NaverIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
		super(session, config);
		
		config.setAuthorizationUrl(AUTH_URL);
		config.setTokenUrl(TOKEN_URL);
		config.setUserInfoUrl(PROFILE_URL);
	}

	@Override
	protected boolean supportsExternalExchange() {
		return true;
	}

	@Override
	protected String getProfileEndpointForValidation(EventBuilder event) {
		return PROFILE_URL;
	}

	@Override
	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
		// getJsonProperty 는 Oidc 관련 파싱만 가능하므로 JsonNode 의 get 메소드를 이용해서 가져온다.
		// BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "response"));
		BrokeredIdentityContext user = new BrokeredIdentityContext(profile.get("response").get("id").asText());

		String email = profile.get("response").get("email").asText();

		user.setIdpConfig(getConfig());
		user.setUsername(email);
		user.setEmail(email);
		user.setIdp(this);

		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());

		return user;
	}

	@Override
	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
		try {
			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).param("access_token", accessToken).asJson();

			BrokeredIdentityContext user = extractIdentityFromProfile(null, profile);

			return user;
		} catch (Exception e) {
			throw new IdentityBrokerException("Could not obtain user profile from naver.", e);
		}
	}

	@Override
	protected String getDefaultScopes() {
		return "";
	}
}

먼저 상수로 선언된 문자열들을 각각 네이버의 인증 URL, 토큰 발급 URL 과 Profile 을 가져오는 URL 들을 설정했다. Scope 는 네이버 개발가이드를 보면 따로 설정을 안해줘도 된다고 해서 작성은 했지만 아래 getDefaultScopes 에서 빈 문자열을 반환하게 했다.

생성자에서는 Identity Provider 의 기본 URL 설정을 해주었다.

  • getProfileEndpointForValidation: 네이버 Profile Endpoint 주소 반환
  • extractIdentityFromProfile: 네이버 Profile 내용 반환
  • doGetFederatedIdentity: 실제로 네이버에 인증 요청을 하고 토큰을 받아오는 역할, 토큰을 이용해 Profile 을 가져오는 역할을 수행하는 메소드이다.
  • getDefaultScopes: 네이버 개발가이드에도 나와있듯이 scope 값이 필요없기 때문에 여기서는 빈 문자열을 반환

User Storage SPI 에 비하면 구현이 비교적 간단하다. 자 그럼 여기서 끝? 위에 프로젝트 구조 그림을 다시보자. src 와 같은 레벨에 resources 디렉토리가 있다. 보면 .html 확장자를 가진 파일 두개가 있다. 이것은 다음 화면을 찾아가는데 필요한 파일이지 않을까 싶다. (아는 분이 있으면 알려주세요..)

주의할 점은 각 html 파일명에서 마지막에 오는 naver 이부분이다. 앞서 Factory 클래스에서 작성한 Id 값을 여기에다 적어줘야 한다. 적어주지 않을 경우 위 그림의 화면으로 연결이 안된다.

이제 프로젝트를 빌드해보자. 여기에서는 Gradle 프로젝트로 개발되었기때문에 “gradle bulid” 를 하자. 그러면 build 디렉토리 아래에 jar 가 생성된다. 그럼 이제 이 jar 를 이전처럼 standalone/deployments 아래에 넣어주자. 그리고 Keycloak 서버가 재 실행 되면 대시보드로 접속해서 Identity Provider 를 보자.

네이버가 잘 추가되었다. 굿! 이제 naver 를 선택해보자 혹시 에러가 발생하거나 페이지 이동이 안된다면 하나 확인해야할 점이 있다. 위에서 말한 .html 파일들을 keycloak 프로젝트에서 themes/base/admin/resources/partials/ 밑에 둬보자. 그런뒤 Keycloak 서버를 재시작하고 다시 naver 를 선택하면 다음과 같은 화면이 나온다.

여기서 입력해줘야할 것은 네이버 개발자에서 발급받은 Client ID, Client Secret 두개뿐이다. 입력이 완료되었으면 네이버 개발자에 등록해야 되는 Callback URL 인 Redirect URL 을 복사해두고 save 를 하자.

마지막으로 네아로(네이버 아이디로 로그인) 개발자 화면에서 이런저런 등록을 하고 API 설정탭에서 다음 두부분을 입력해줘야 한다. 하나는 서비스가 실행되는 URL 과 방금 복사한 Redirect URL 을 넣어줄 Callback URl 이다. Callback URL 에 복사한 Redirect URL 을 넣어주고 서비스 URL 도 마저 채워주자.

테스트를 위해 다음 URL 로 접속을 하자 http://localhost:8080/auth/realms/{realm}/account/

그러면 다음과 같은 로그인 화면이 나오고 네이버를 선택해 네이버 로그인을 진행하면 된다.


마치며


OIDC 와 OAuth 의 차이점을 공부한 내용도 올려야겠다. 점점 OIDC 로 변하는 추세여서 그런지 오픈소스들도 속속들이 반영을 하는가보다.

댓글남기기