Chrome 확장 애플리케이션에 인증 (Authentication) 추가하세요
이 가이드는 Logto를 Chrome 확장에 통합하는 방법을 보여줍니다.
- 다음 시연은 Chrome v123.0.6312.87 (arm64)에서 테스트되었습니다. SDK에서 사용된
chrome
API를 지원하는 한, 다른 버전에서도 작동해야 합니다. - 샘플 프로젝트는 우리의 GitHub 저장소에서 확인할 수 있습니다.
사전 준비 사항
- Logto Cloud 계정 또는 셀프 호스팅 Logto.
- Logto Console에서 생성된 단일 페이지 애플리케이션 (SPA).
- Chrome 확장 프로젝트.
설치
- npm
- Yarn
- pnpm
npm i @logto/chrome-extension
yarn add @logto/chrome-extension
pnpm add @logto/chrome-extension
통합
인증 흐름
Chrome 확장의 팝업에 "로그인" 버튼을 넣었다고 가정하면, 인증 흐름은 다음과 같습니다:
확장의 다른 상호작용 페이지에 대해서는 확장 팝업
참가자를 페이지 이름으로 교체하면 됩니다. 이 튜토리얼에서는 팝업 페이지에 집중할 것입니다.
리디렉션 기반 로그인에 관하여
- 이 인증 과정은 OpenID Connect (OIDC) 프로토콜을 따르며, Logto는 사용자 로그인을 보호하기 위해 엄격한 보안 조치를 시행합니다.
- 여러 앱이 있는 경우, 동일한 아이덴티티 제공자 (Logto)를 사용할 수 있습니다. 사용자가 한 앱에 로그인하면, Logto는 사용자가 다른 앱에 접근할 때 자동으로 로그인 과정을 완료합니다.
리디렉션 기반 로그인에 대한 이론적 배경과 이점에 대해 더 알고 싶다면, Logto 로그인 경험 설명을 참조하세요.
manifest.json
업데이트
Logto SDK는 manifest.json
에서 다음 권한이 필요합니다:
{
"permissions": ["identity", "storage"],
"host_permissions": ["https://*.logto.app/*"]
}
permissions.identity
: 로그인 및 로그아웃에 사용되는 Chrome Identity API에 필요합니다.permissions.storage
: 사용자의 세션을 저장하는 데 필요합니다.host_permissions
: Logto SDK가 Logto API와 통신하는 데 필요합니다.
Logto Cloud에서 사용자 지정 도메인을 사용하는 경우, host_permissions
를 도메인에 맞게 업데이트해야 합니다.
백그라운드 스크립트 (서비스 워커) 설정
Chrome 확장의 백그라운드 스크립트에서 Logto SDK를 초기화하세요:
import LogtoClient from '@logto/chrome-extension';
export const logtoClient = new LogtoClient({
endpoint: '<your-logto-endpoint>'
appId: '<your-logto-app-id>',
});
<your-logto-endpoint>
및 <your-logto-app-id>
를 실제 값으로 교체하세요. 이 값들은 Logto Console에서 방금 생성한 애플리케이션 페이지에서 찾을 수 있습니다.
백그라운드 스크립트가 없는 경우, 공식 가이드를 따라 하나를 생성할 수 있습니다.
왜 백그라운드 스크립트가 필요한가요?
팝업이나 옵션 페이지와 같은 일반 확장 페이지는 백그라운드에서 실행될 수 없으며, 인증 과정 중에 닫힐 가능성이 있습니다. 백그라운드 스크립트는 인증 과정을 적절히 처리할 수 있도록 보장합니다.
그런 다음, 다른 확장 페이지로부터 메시지를 듣고 인증 과정을 처리해야 합니다:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 아래 코드에서는 각 작업에 대해 `true`를 반환하므로, `sendResponse`를 호출하여
// 발신자에게 알림을 보내야 합니다. 여기서 오류를 처리하거나 다른 방법으로 발신자에게 알릴 수도 있습니다.
if (message.action === 'signIn') {
const redirectUri = chrome.identity.getRedirectURL('/callback');
logtoClient.signIn(redirectUri).finally(sendResponse);
return true;
}
if (message.action === 'signOut') {
const redirectUri = chrome.identity.getRedirectURL();
logtoClient.signOut(redirectUri).finally(sendResponse);
return true;
}
return false;
});
위 코드에서 두 개의 리디렉션 URI가 사용된 것을 알 수 있습니다. 이들은 모두 chrome.identity.getRedirectURL
에 의해 생성되며, 이는 인증 흐름을 위한 리디렉션 URL을 생성하는 Chrome 내장 API입니다. 두 URI는 다음과 같습니다:
- 로그인용
https://<extension-id>.chromiumapp.org/callback
. - 로그아웃용
https://<extension-id>.chromiumapp.org/
.
이 URI들은 접근할 수 없으며, Chrome이 인증 과정의 특정 작업을 트리거하기 위해 사용됩니다.
Logto 애플리케이션 설정 업데이트
이제 방금 생성한 리디렉션 URI를 허용하도록 Logto 애플리케이션 설정을 업데이트해야 합니다.
- Logto Console에서 애플리케이션 페이지로 이동합니다.
- "리디렉션 URI" 섹션에서 URI를 추가합니다:
https://<extension-id>.chromiumapp.org/callback
. - "로그아웃 후 리디렉션 URI" 섹션에서 URI를 추가합니다:
https://<extension-id>.chromiumapp.org/
. - "CORS 허용 원본" 섹션에서 URI를 추가합니다:
chrome-extension://<extension-id>
. Chrome 확장에서 SDK는 이 원본을 사용하여 Logto API와 통신합니다. - 변경 사항 저장을 클릭합니다.
<extension-id>
를 실제 확장 ID로 교체하는 것을 잊지 마세요. 확장 ID는 chrome://extensions
페이지에서 찾을 수 있습니다.
팝업에 로그인 및 로그아웃 버튼 추가
거의 다 왔습니다! 팝업 페이지에 로그인 및 로그아웃 버튼과 기타 필요한 로직을 추가해 봅시다.
popup.html
파일에서:
<button id="sign-in">로그인</button> <button id="sign-out">로그아웃</button>
popup.js
파일에서 (popup.js
가 popup.html
에 포함되어 있다고 가정):
document.getElementById('sign-in').addEventListener('click', async () => {
await chrome.runtime.sendMessage({ action: 'signIn' });
// 로그인 완료 (또는 실패), 여기서 UI를 업데이트할 수 있습니다.
});
document.getElementById('sign-out').addEventListener('click', async () => {
await chrome.runtime.sendMessage({ action: 'signOut' });
// 로그아웃 완료 (또는 실패), 여기서 UI를 업데이트할 수 있습니다.
});
체크포인트: 인증 흐름 테스트
이제 Chrome 확장에서 인증 흐름을 테스트할 수 있습니다:
- 확장 팝업을 엽니다.
- "로그인" 버튼을 클릭합니다.
- Logto 로그인 페이지로 리디렉션됩니다.
- Logto 계정으로 로그인합니다.
- Chrome으로 다시 리디렉션됩니다.
인증 상태 확인
Chrome은 통합된 저장소 API를 제공하므로, 로그인 및 로그아웃 흐름 외에도 모든 Logto SDK 메서드를 팝업 페이지에서 직접 사용할 수 있습니다.
popup.js
에서, 백그라운드 스크립트에서 생성한 LogtoClient
인스턴스를 재사용하거나 동일한 구성으로 새로 생성할 수 있습니다:
import LogtoClient from '@logto/chrome-extension';
const logtoClient = new LogtoClient({
endpoint: '<your-logto-endpoint>'
appId: '<your-logto-app-id>',
});
// 또는 백그라운드 스크립트에서 생성한 logtoClient 인스턴스를 재사용
import { logtoClient } from './service-worker.js';
그런 다음 인증 상태와 사용자의 프로필을 로드하는 함수를 생성할 수 있습니다:
const loadAuthenticationState = async () => {
const isAuthenticated = await logtoClient.isAuthenticated();
// 인증 상태에 따라 UI를 업데이트합니다.
if (isAuthenticated) {
const user = await logtoClient.getIdTokenClaims(); // { sub: '...', email: '...', ... }
// 사용자의 프로필로 UI를 업데이트합니다.
}
};
loadAuthenticationState
함수를 로그인 및 로그아웃 로직과 결합할 수도 있습니다:
document.getElementById('sign-in').addEventListener('click', async () => {
await chrome.runtime.sendMessage({ action: 'signIn' });
await loadAuthenticationState();
});
document.getElementById('sign-out').addEventListener('click', async () => {
await chrome.runtime.sendMessage({ action: 'signOut' });
await loadAuthenticationState();
});
다음은 인증 상태가 포함된 팝업 페이지의 예입니다:

기타 고려 사항
- 서비스 워커 번들링: Webpack이나 Rollup과 같은 번들러를 사용하는 경우, Node.js 모듈의 불필요한 번들링을 피하기 위해 명시적으로
browser
또는 유사한 대상으로 설정해야 합니다. - 모듈 해상도: Logto Chrome 확장 SDK는 ESM 전용 모듈입니다.
TypeScript, Rollup 및 기타 구성과 함께 완전한 예제를 보려면 샘플 프로젝트를 참조하세요.
사용자 정보 가져오기
사용자 정보 표시
사용자의 정보를 표시하려면 logtoClient.getIdTokenClaims()
메서드를 사용할 수 있습니다. 예를 들어, Home 페이지에서:
const userInfo = await logtoClient.getIdTokenClaims();
// ID 토큰 클레임 (Claims)을 위한 표시 테이블 생성
const table = document.createElement('table');
const thead = document.createElement('thead');
const tr = document.createElement('tr');
const thName = document.createElement('th');
const thValue = document.createElement('th');
thName.innerHTML = 'Name';
thValue.innerHTML = 'Value';
tr.append(thName, thValue);
thead.append(tr);
table.append(thead);
const tbody = document.createElement('tbody');
for (const [key, value] of Object.entries(userInfo)) {
const tr = document.createElement('tr');
const tdName = document.createElement('td');
const tdValue = document.createElement('td');
tdName.innerHTML = key;
tdValue.innerHTML = typeof value === 'string' ? value : JSON.stringify(value);
tr.append(tdName, tdValue);
tbody.append(tr);
}
table.append(tbody);
추가 클레임 (Claims) 요청
getIdTokenClaims()
에서 반환된 객체에 일부 사용자 정보가 누락된 것을 발견할 수 있습니다.
이는 OAuth 2.0 및 OpenID Connect (OIDC)가 최소 권한 원칙 (PoLP)을 따르도록 설계되었기 때문이며,
Logto는 이러한 표준을 기반으로 구축되었습니다.
기본적으로 제한된 클레임 (Claim)만 반환됩니다. 더 많은 정보를 원하시면, 추가적인 스코프 (Scope)를 요청하여 더 많은 클레임에 접근할 수 있습니다.
"클레임 (Claim)"은 주체에 대해 주장하는 내용이며, "스코프 (Scope)"는 클레임의 그룹입니다. 현재의 경우, 클레임은 사용자에 대한 정보입니다.
다음은 스코프 - 클레임 관계의 비규범적 예시입니다:
"sub" 클레임은 "주체"를 의미하며, 이는 사용자의 고유 식별자 (즉, 사용자 ID)입니다.
Logto SDK는 항상 세 가지 스코프를 요청합니다: openid
, profile
, 그리고 offline_access
.
추가 스코프 (Scopes)를 요청하려면 Logto 설정을 구성할 수 있습니다:
import LogtoClient, { UserScope } from '@logto/browser';
const logtoClient = new LogtoClient({
appId: '<your-application-id>',
endpoint: '<your-logto-endpoint>',
scopes: [UserScope.Email, UserScope.Phone],
});
그런 다음 logtoClient.getIdTokenClaims()
의 반환 값에서 추가 클레임 (Claims)에 접근할 수 있습니다:
const claims = await getIdTokenClaims();
// 이제 추가 클레임 (Claims) `claims.email`, `claims.phone` 등에 접근할 수 있습니다.
네트워크 요청이 필요한 클레임
ID 토큰의 비대화를 방지하기 위해, 일부 클레임은 네트워크 요청을 통해 가져와야 합니다. 예를 들어, custom_data
클레임은 스코프에서 요청되더라도 사용자 객체에 포함되지 않습니다. 이러한 클레임에 접근하려면, logtoClient.fetchUserInfo()
메서드를 사용할 수 있습니다:
const userInfo = await logtoClient.fetchUserInfo();
// 이제 클레임 (Claim) `userInfo.custom_data`에 접근할 수 있습니다.
스코프 (Scopes)와 클레임 (Claims)
Logto는 OIDC 스코프 및 클레임 규약을 사용하여 ID 토큰 및 OIDC userinfo 엔드포인트에서 사용자 정보를 가져오기 위한 스코프와 클레임을 정의합니다. "스코프"와 "클레임"은 OAuth 2.0 및 OpenID Connect (OIDC) 사양의 용어입니다.
지원되는 스코프와 해당 클레임 (Claims) 목록은 다음과 같습니다:
openid
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
sub | string | 사용자의 고유 식별자 | 아니요 |
profile
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
name | string | 사용자의 전체 이름 | 아니요 |
username | string | 사용자의 사용자 이름 | 아니요 |
picture | string | 최종 사용자의 프로필 사진 URL. 이 URL은 이미지 파일 (예: PNG, JPEG, 또는 GIF 이미지 파일)을 참조해야 하며, 이미지를 포함하는 웹 페이지를 참조해서는 안 됩니다. 이 URL은 최종 사용자를 설명할 때 표시하기 적합한 프로필 사진을 구체적으로 참조해야 하며, 최종 사용자가 찍은 임의의 사진을 참조해서는 안 됩니다. | 아니요 |
created_at | number | 최종 사용자가 생성된 시간. 시간은 유닉스 에포크 (1970-01-01T00:00:00Z) 이후 밀리초 수로 표현됩니다. | 아니요 |
updated_at | number | 최종 사용자의 정보가 마지막으로 업데이트된 시간. 시간은 유닉스 에포크 (1970-01-01T00:00:00Z) 이후 밀리초 수로 표현됩니다. | 아니요 |
다른 표준 클레임인 family_name
, given_name
, middle_name
, nickname
, preferred_username
, profile
, website
, gender
, birthdate
, zoneinfo
, 및 locale
도 사용자 정보 엔드포인트를 요청할 필요 없이 profile
스코프에 포함됩니다. 위의 클레임과의 차이점은 이러한 클레임은 값이 비어 있지 않을 때만 반환되며, 위의 클레임은 값이 비어 있을 경우 null
을 반환한다는 점입니다.
표준 클레임과 달리, created_at
및 updated_at
클레임은 초 대신 밀리초를 사용합니다.
email
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
string | 사용자의 이메일 주소 | 아니요 | |
email_verified | boolean | 이메일 주소가 검증되었는지 여부 | 아니요 |
phone
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
phone_number | string | 사용자의 전화번호 | 아니요 |
phone_number_verified | boolean | 전화번호가 검증되었는지 여부 | 아니요 |
address
주소 클레임의 세부 사항은 OpenID Connect Core 1.0을 참조하세요.
custom_data
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
custom_data | object | 사용자의 사용자 정의 데이터 | 예 |
identities
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
identities | object | 사용자의 연결된 아이덴티티 | 예 |
sso_identities | array | 사용자의 연결된 SSO 아이덴티티 | 예 |
roles
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
roles | string[] | 사용자의 역할 | 아니요 |
urn:logto:scope:organizations
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
organizations | string[] | 사용자가 속한 조직 ID | 아니요 |
organization_data | object[] | 사용자가 속한 조직 데이터 | 예 |
urn:logto:scope:organization_roles
클레임 이름 | 유형 | 설명 | 사용자 정보 필요 여부 |
---|---|---|---|
organization_roles | string[] | 사용자가 속한 조직 역할, 형식은 <organization_id>:<role_name> | 아니요 |
성능과 데이터 크기를 고려할 때, "사용자 정보 필요 여부"가 "예"인 경우, 해당 클레임은 ID 토큰에 나타나지 않으며, userinfo 엔드포인트 응답에서 반환됩니다.
API 리소스
먼저 🔐 역할 기반 접근 제어 (RBAC)를 읽어 Logto RBAC의 기본 개념과 API 리소스를 적절히 설정하는 방법을 이해하는 것을 권장합니다.
Logto 클라이언트 구성하기
API 리소스를 설정한 후, 애플리케이션에서 Logto를 구성할 때 이를 추가할 수 있습니다:
import LogtoClient from '@logto/browser';
const logtoClient = new LogtoClient({
// ...other configs
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // API 리소스를 추가하세요
});
각 API 리소스는 자체 권한 (스코프)을 가지고 있습니다.
예를 들어, https://shopping.your-app.com/api
리소스는 shopping:read
및 shopping:write
권한을 가지고 있으며, https://store.your-app.com/api
리소스는 store:read
및 store:write
권한을 가지고 있습니다.
이러한 권한을 요청하려면, 애플리케이션에서 Logto를 구성할 때 추가할 수 있습니다:
import LogtoClient from '@logto/chrome-extension';
const logtoClient = new LogtoClient({
// ...other configs
scopes: ['shopping:read', 'shopping:write', 'store:read', 'store:write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // API 리소스를 추가하세요
});
스코프가 API 리소스와 별도로 정의된 것을 알 수 있습니다. 이는 OAuth 2.0을 위한 리소스 지표가 요청의 최종 스코프가 모든 대상 서비스의 모든 스코프의 데카르트 곱이 될 것이라고 명시하기 때문입니다.
따라서 위의 경우, Logto에서 정의된 스코프를 단순화할 수 있으며, 두 API 리소스 모두 접두사 없이 read
및 write
스코프를 가질 수 있습니다. 그런 다음, Logto 구성에서:
import LogtoClient, { UserScope } from '@logto/chrome-extension';
const logtoClient = new LogtoClient({
// ...other configs
scopes: ['read', 'write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
});
모든 API 리소스에 대해 read
및 write
스코프를 요청하게 됩니다.
API 리소스에 정의되지 않은 스코프를 요청해도 괜찮습니다. 예를 들어, API 리소스에 email
스코프가 없더라도 email
스코프를 요청할 수 있습니다. 사용 불가능한 스코프는 안전하게 무시됩니다.
성공적으로 로그인한 후, Logto는 사용자의 역할에 따라 API 리소스에 적절한 스코프를 발급합니다.
API 리소스를 위한 액세스 토큰 가져오기
특정 API 리소스에 대한 액세스 토큰을 가져오려면 getAccessToken
메서드를 사용할 수 있습니다:
const accessToken = await logtoClient.getAccessToken('https://store.your-app.com/api');
console.log('액세스 토큰', accessToken);
이 메서드는 사용자가 관련 권한을 가지고 있을 때 API 리소스에 접근할 수 있는 JWT 액세스 토큰을 반환합니다. 현재 캐시된 액세스 토큰이 만료된 경우, 이 메서드는 자동으로 리프레시 토큰을 사용하여 새로운 액세스 토큰을 얻으려고 시도합니다.
조직 토큰 가져오기
조직이 처음이라면, 시작하기 위해 🏢 조직 (다중 테넌시)을 읽어보세요.
Logto 클라이언트를 구성할 때 UserScope.Organizations
스코프를 추가해야 합니다:
import LogtoClient, { UserScope } from '@logto/chrome-extension';
const logtoClient = new LogtoClient({
// ...other configs
scopes: [UserScope.Organizations],
});
사용자가 로그인하면, 사용자에 대한 조직 토큰을 가져올 수 있습니다:
// 사용자 정보에서 organizationIds 가져오기
const claims = await logtoClient.getIdTokenClaims();
const organizationIds = claims.organizations;
/**
* 또는 ID 토큰 클레임에서 가져오기
*
* const claims = await logtoClient.getIdTokenClaims();
* const organizationIds = claims.organizations;
*/
// 조직 액세스 토큰 가져오기
if (organizationIds.length > 0) {
const organizationId = organizationIds[0];
const organizationAccessToken = await logtoClient.getOrganizationToken(organizationId);
console.log('조직 액세스 토큰', organizationAccessToken);
}
./code/_scopes-and-claims-code.mdx./code/_config-organization-code.mdx
요청 헤더에 액세스 토큰 첨부하기
토큰을 HTTP 헤더의 Authorization
필드에 Bearer 형식 (Bearer YOUR_TOKEN
)으로 넣으면 됩니다.
Bearer 토큰의 통합 흐름은 사용 중인 프레임워크나 요청자에 따라 다를 수 있습니다. 요청 Authorization
헤더를 적용하는 방법을 선택하세요.