# MyIAM 전체 문서 (AI 참고용) > 이 파일은 빌드 스크립트(scripts/generate-llms-doc.mjs)가 실제 문서 페이지를 렌더링해 자동 생성합니다. 직접 수정하지 마세요. > 인덱스: https://myiam.io/llms.txt > 아래 각 섹션의 `URL:` 값(예: https://myiam.io/docs/...)은 출처 표시용입니다. 해당 페이지는 JavaScript로 렌더링되는 SPA라 크롤러나 AI 도구가 직접 방문해도 본문을 읽을 수 없습니다. 다시 방문하지 말고, 이 파일에 이미 포함된 내용을 참고하세요. > 반면 github.com 링크(샘플 코드 저장소)는 정적으로 렌더링되는 일반 저장소이므로 실제로 방문해 전체 예제 코드를 참고하는 것이 좋습니다. --- ## 문서 URL: https://myiam.io/docs MyIAM 문서 - 시작하기 가이드와 API 문서를 확인하세요. [NEW AI 에이전트용 MyIAM CLI가 추가되었습니다](https://myiam.io/docs/cli/install) # MyIAM 시작하기 MyIAM을 사용하여 서비스에 인증과 회원 관리 기능을 빠르게 통합하세요. 플랫폼별 시작 가이드와 API 레퍼런스를 제공합니다. ## CLI NEW AI 코딩 에이전트와 함께 쓰면 서비스를 만들고, 설정하고, 고치는 일을 터미널에서 말 몇 마디로 끝낼 수 있습니다. [MyIAM CLIClaude Code, Codex CLI, Antigravity CLI용 AI 에이전트 플러그인 설치 가이드](https://myiam.io/docs/cli/install) ## 시작하기 플랫폼을 선택하고 단계별 가이드를 따라 MyIAM을 연동하세요. [React`TypeScript`클라이언트 SDK로 SPA 인증](https://myiam.io/docs/quickstart/react)[Next.js`TypeScript`서버 SDK로 SSR 인증](https://myiam.io/docs/quickstart/nextjs)[Flutter`Dart`클라이언트 SDK로 모바일 앱 인증](https://myiam.io/docs/quickstart/flutter)[Spring Boot`Kotlin / Java`Spring Security로 서버 인증](https://myiam.io/docs/quickstart/spring-boot)[Expo`TypeScript`React Native 앱 인증](https://myiam.io/docs/quickstart/expo) ### Vue 준비 중 `TypeScript`클라이언트 SDK로 SPA 인증 ### NestJS 준비 중 `TypeScript`서버 SDK로 백엔드 인증 ### iOS 준비 중 `Swift`네이티브 모바일 앱 인증 ### Android 준비 중 `Kotlin`네이티브 모바일 앱 인증 ## 관리자 가이드 NEW 관리자 콘솔에서 서비스를 설정하고 관리하는 방법을 안내합니다. [서비스 관리서비스 설정, OAuth2, SDK, 로그인 방법, 약관, 정책, 사용자 정보](https://myiam.io/docs/admin/service) ## 통합 가이드 외부 서비스와 MyIAM을 함께 사용하는 방법을 안내합니다. [Firebase 연동Flutter SDK + Web SDK로 Firebase 서비스 연동](https://myiam.io/docs/guide/firebase-flutter)[Supabase 연동Flutter SDK + Edge Function으로 Supabase 서비스 연동](https://myiam.io/docs/guide/supabase-flutter) ## HTTP API MyIAM의 HTTP API 엔드포인트를 확인하세요. [HTTP API 레퍼런스회원, 토큰, 사용자, 서비스 사용자 API](https://myiam.io/docs/api) ## SDK 플랫폼별 SDK 문서입니다. [Web SDK브라우저 + 서버 인증 SDK, 릴리스 노트](https://myiam.io/docs/sdk/web)[Expo SDKExpo(React Native) 인증 SDK, 릴리스 노트](https://myiam.io/docs/sdk/expo)[Flutter SDKFlutter 인증 + REST API SDK, 릴리스 노트](https://myiam.io/docs/sdk/flutter)[Firebase Flutter SDKMyIAM + Firebase 통합 SDK, 릴리스 노트](https://myiam.io/docs/sdk/flutter/firebase)[Supabase Flutter SDKMyIAM + Supabase 통합 SDK, 릴리스 노트](https://myiam.io/docs/sdk/flutter/supabase) --- ## Expo로 시작하기 URL: https://myiam.io/docs/quickstart/expo Expo(React Native) 앱에 MyIAM 로그인, 회원가입, 로그아웃 기능을 추가하는 방법을 안내합니다. 샘플 코드: https://github.com/myiam-io/expo-samples # Expo로 시작하기 이 가이드는 Expo, React Native, TypeScript 환경 기준입니다. Expo(React Native) 앱에 로그인, 회원가입, 로그아웃 기능을 추가하는 방법을 안내합니다. 처음이라도 코드를 복사해서 붙여넣기만 하면 동작하는 샘플 앱을 만들 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - **Node.js 20 이상**이 설치되어 있어야 합니다. 터미널에서 `node -v`로 확인할 수 있습니다. > - **iOS는 macOS + Xcode**, **Android는 Android Studio**가 필요합니다. 없다면 [EAS Build](https://docs.expo.dev/build/introduction/)로 클라우드 빌드를 사용할 수 있습니다. **Expo Go로는 이 가이드를 완주할 수 없습니다.** Expo Go는 여러 개발자의 앱이 공유하는 컨테이너라 앱 고유의 URL 스킴을 가질 수 없어, 인증 후 앱으로 돌아오는 주소가 `exp://192.168.0.10:8081/--/oauth2callback`처럼 개발 PC의 IP에 묶입니다. 이 주소는 네트워크가 바뀌면 같이 바뀌므로 콘솔에 등록해 둘 수도 없습니다. 아래 7단계의 **개발 빌드(Development Build)** 를 사용하세요. ### Step 1: 프로젝트 생성 터미널을 열고 아래 명령어를 실행하면 `my-app`이라는 새 프로젝트 폴더가 만들어집니다. `터미널` ```bash npx create-expo-app@latest my-app --template blank-typescript cd my-app ``` ### Step 2: SDK 설치 Expo가 관리하는 네이티브 모듈은 `npx expo install`로 설치합니다. Expo SDK 버전에 맞는 버전을 자동으로 골라주므로, 이 네 개는 `npm install`을 쓰지 마세요. `터미널` ```bash npx expo install expo-auth-session expo-web-browser expo-secure-store expo-dev-client ``` MyIAM SDK는 순수 JavaScript 패키지라 일반 설치입니다. `터미널` ```bash npm install @myiam.io/expo-sdk ``` - `@myiam.io/expo-sdk` — MyIAM 인증 기능을 사용하기 위한 SDK - `expo-auth-session` — OAuth2 PKCE 생성, state 검증, 토큰 교환·갱신 - `expo-web-browser` — 시스템 브라우저(iOS ASWebAuthenticationSession / Android Chrome Custom Tabs) - `expo-secure-store` — 토큰 암호화 저장 (iOS Keychain / Android EncryptedSharedPreferences) - `expo-dev-client` — 개발 빌드에 Expo 개발 메뉴 추가 ### Step 3: 딥링크 설정 OAuth2 인증 후 앱으로 돌아오려면 앱 고유의 URL 스킴이 필요합니다. Flutter나 네이티브와 달리 **`AndroidManifest.xml`이나 `Info.plist`를 직접 편집하지 않습니다.** `app.json`에 `scheme` 한 줄만 적으면 빌드할 때 Expo가 양쪽 네이티브 설정을 자동으로 만들어 줍니다. `app.json` ```json { "expo": { "name": "MyIAM Expo Sample", "slug": "myiam-expo-sample", "scheme": "myiamsample", "ios": { "bundleIdentifier": "io.myiam.expo.sample" }, "android": { "package": "io.myiam.expo.sample" } } } ``` 이제 앱의 콜백 주소는 **`myiamsample://oauth2callback`** 이 됩니다. `myiamsample`은 예시이므로 앱에 맞는 고유한 스킴을 사용하세요. > **설정 필요** > > 아래 딥링크 주소를 **Redirect Url**에 등록하세요. 앱의 URL Scheme을 변경했다면 그에 맞게 수정합니다. > URL: `myiamsample://oauth2callback` ### Step 4: 환경 변수 설정 프로젝트 최상위 폴더에 `.env` 파일을 만들고, 내 서비스 정보를 입력합니다. Expo는 `EXPO_PUBLIC_` 접두사가 붙은 변수를 앱 코드에 자동으로 주입합니다. - `EXPO_PUBLIC_MYIAM_SERVICE_UID` = `` 서비스 설정 페이지에서 확인할 수 있는 서비스 고유 식별자입니다. - `EXPO_PUBLIC_MYIAM_CLIENT_ID` = `` OAuth2 설정 페이지에서 확인할 수 있는 클라이언트 식별자입니다. - `EXPO_PUBLIC_MYIAM_API_KEY` = `` 서비스 설정에서 API 키를 생성하면 확인할 수 있습니다. 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. `.env` ```env EXPO_PUBLIC_MYIAM_SERVICE_UID= EXPO_PUBLIC_MYIAM_CLIENT_ID= EXPO_PUBLIC_MYIAM_API_KEY= ``` `.env`는 `.gitignore`에 추가하여 저장소에 포함되지 않도록 합니다. `EXPO_PUBLIC_` 변수는 앱 번들에 그대로 포함되어 추출될 수 있습니다. API 키를 앱에 두는 것이 부담스럽다면 `apiKey`를 생략하고 `resolveUser` 옵션으로 자체 백엔드를 경유시킬 수 있습니다. 단 회원가입 완료와 사용자 액션 기능에는 API 키가 필요합니다. ### Step 5: SDK 초기화 앱 최상단을 `MyiamProvider`로 감쌉니다. 여기서 넘긴 `scheme` + `path`가 3단계에서 설정한 `myiamsample://oauth2callback`을 만듭니다. `App.tsx` ```react import { MyiamProvider } from "@myiam.io/expo-sdk" import Root from "./src/Root" export default function App() { return ( ) } ``` `debug: __DEV__`를 켜면 개발 중에만 `[MyIAM]` 접두사로 인증 흐름 로그가 콘솔에 출력됩니다. 인증 상태에 따라 화면을 전환합니다. `src/Root.tsx` ```react import { useMyiam } from "@myiam.io/expo-sdk" import { ActivityIndicator, View } from "react-native" import Dashboard from "./Dashboard" import SignIn from "./SignIn" export default function Root() { const { status } = useMyiam() if (status === "loading") { return ( ) } return status === "authenticated" ? : } ``` `status`는 `"loading"`, `"authenticated"`, `"unauthenticated"` 세 가지입니다. 앱 시작 시 저장된 세션을 복원하는 동안이 `loading`이고, 이후 자동으로 전환됩니다. 별도의 상태 관리 라이브러리가 필요 없습니다. ### Step 6: 홈 화면 — 로그인 / 회원가입 `login()`과 `signup()`은 시스템 브라우저를 띄우고, 인증이 끝나 앱으로 돌아오면 사용자 정보를 담은 Promise를 반환합니다. 사용자가 브라우저를 그냥 닫으면 `null`을 반환합니다. `src/SignIn.tsx` ```react import { useMyiam } from "@myiam.io/expo-sdk" import { useState } from "react" import { Button, SafeAreaView, StyleSheet, Text, View } from "react-native" export default function SignIn() { const { login, signup } = useMyiam() const [error, setError] = useState(null) const [busy, setBusy] = useState(false) const run = async (fn: () => Promise) => { setError(null) setBusy(true) try { await fn() // 성공하면 status가 authenticated로 바뀌어 Root가 알아서 화면을 바꾼다. } catch (e) { setError(String(e)) } finally { setBusy(false) } } return ( MyIAM Expo ) } ``` **회원가입도 같은 `/callback`으로 끝납니다.** MyIAM은 가입이 끝나면 같은 인증 요청을 그대로 이어받아 자동으로 로그인시키고 콜백 주소로 돌려보냅니다. 가입용 콜백을 따로 만들 필요가 없습니다. 가입을 마쳤는데 로그인 화면이 한 번 더 뜬다면 서비스 > 서비스 정보 페이지의 회원가입 설정에서 **자동 로그인**이 꺼져 있는 것입니다 (기본값 ON). ### Step 7: 콜백 페이지 사용자가 로그인을 마치면 MyIAM 서버가 이 페이지로 되돌려 보냅니다. 여기서 로그인 결과(토큰)를 받아 저장하고, 대시보드로 이동합니다. 아래 예시는 토큰을 `sessionStorage`에 저장합니다. 브라우저 저장소는 XSS에 노출되므로, 특히 리프레시 토큰을 다루는 프로덕션 앱이라면 토큰을 HttpOnly 쿠키에 담아 서버가 관리하는 [Server SDK](https://myiam.io/docs/sdk/web/server) 방식을 권장합니다. `src/pages/CallbackPage.tsx` ```react import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { myiamHandleCallback, MyiamCallbackError } from '@myiam.io/web-sdk/client' export default function CallbackPage() { const navigate = useNavigate() const called = useRef(false) useEffect(() => { // 한 번만 실행되도록 방지 if (called.current) return called.current = true // MyIAM 서버에서 받은 인증 코드를 토큰으로 교환 myiamHandleCallback() .then((tokens) => { // 받은 토큰을 브라우저에 저장 sessionStorage.setItem('access_token', tokens.access_token) sessionStorage.setItem('refresh_token', tokens.refresh_token) // 대시보드로 이동 navigate('/dashboard') }) .catch((err) => { // 실패 이유를 남기지 않으면 "콜백에서 멈췄다"로만 보입니다 const code = err instanceof MyiamCallbackError ? err.code : 'unknown' console.error('[myiam] callback failed:', code, err) navigate(`/?error=${code}`) }) }, [navigate]) return

로그인 처리 중...

} ``` `MyiamCallbackError`의 `code`로 실패 원인이 갈립니다. 전체 목록은 [Client SDK의 MyiamCallbackError](https://myiam.io/docs/sdk/web/client)에 있고, 개발 중에 가장 자주 만나는 둘은 이렇습니다. | `code` | 원인 | | --- | --- | | `missing_pkce_state` | 로그인을 시작한 지 30분이 지났거나, 다른 탭에서 로그인을 다시 시작했습니다. 다시 로그인하면 됩니다 | | `authorization_error` | 인증 서버가 요청을 거절했습니다. 대부분 **Redirect Url 등록값과 앱이 보낸 주소가 한 글자라도 다른 경우**입니다 | ### Step 8: 대시보드 페이지 로그인에 성공하면 보여줄 화면입니다. 토큰 갱신과 로그아웃 버튼을 만들어 봅니다. > **설정 필요** > > 아래 개발용 주소를 **Logout Redirect Url**에 등록하세요. 배포 시에는 실제 도메인으로 변경합니다. > URL: `http://localhost:5173` `src/pages/DashboardPage.tsx` ```react import { useState } from 'react' import { myiamLogoutUrl, myiamRefreshToken } from '@myiam.io/web-sdk/client' export default function DashboardPage() { const [accessToken, setAccessToken] = useState( () => sessionStorage.getItem('access_token'), ) const [refreshToken, setRefreshToken] = useState( () => sessionStorage.getItem('refresh_token'), ) // 토큰이 만료되었을 때 새 토큰을 발급받습니다 async function handleRefresh() { if (!refreshToken) return const tokens = await myiamRefreshToken(refreshToken) sessionStorage.setItem('access_token', tokens.access_token) sessionStorage.setItem('refresh_token', tokens.refresh_token) setAccessToken(tokens.access_token) setRefreshToken(tokens.refresh_token) } // 로그아웃 후 첫 화면으로 돌아갑니다 function handleLogout() { sessionStorage.removeItem('access_token') sessionStorage.removeItem('refresh_token') window.location.href = myiamLogoutUrl(window.location.origin) } return (

대시보드

Access Token: {accessToken?.slice(0, 20)}...

Refresh Token: {refreshToken?.slice(0, 20)}...

) } ``` ### Step 9: 실행 모든 파일을 저장하고 아래 명령어로 개발 서버를 시작합니다. `터미널` ```bash npm run dev ``` 브라우저에서 `http://localhost:5173`을 열면 로그인 버튼이 보입니다. 버튼을 클릭해서 로그인이 잘 되는지 확인해 보세요! **전체 인증 흐름** ```mermaid flowchart TD A["로그인 버튼 클릭"] A --> B["MyIAM 로그인 화면으로 이동
SDK가 안전한 인증 URL을 만들어 줍니다"] B --> C["사용자가 로그인 완료
MyIAM 서버에서 아이디/비밀번호를 확인합니다"] C --> D["우리 앱의 /callback 페이지로 돌아옴"] D --> E["토큰 발급
SDK가 인증 결과를 토큰으로 교환합니다"] E --> F["대시보드 진입
토큰을 저장하고 로그인 완료!"] G["회원가입 버튼 클릭"] G --> H["MyIAM 회원가입 화면으로 이동
약관 동의 → 정보 입력 순서로 진행됩니다"] H --> I["가입 완료 후 자동 로그인
서비스 정보의 자동 로그인 설정이 ON일 때 (기본값)"] I --> J["같은 /callback 페이지로 돌아옴
여기서부터는 로그인과 완전히 같습니다"] K["로그아웃 버튼 클릭"] K --> L["MyIAM 서버로 로그아웃 요청
세션을 정리하고 로그아웃을 처리합니다"] L --> M["설정한 Logout Redirect Url로 돌아옴
로그아웃 완료! 첫 화면으로 이동합니다"] ``` **프로젝트 구조** ``` my-app/ ├── src/ │ ├── main.tsx -- SDK 초기화 + 렌더링 │ ├── App.tsx -- 페이지 연결 │ └── pages/ │ ├── HomePage.tsx -- 로그인/회원가입 화면 │ ├── CallbackPage.tsx -- 로그인 완료 처리 │ └── DashboardPage.tsx -- 로그인 후 화면 ├── .env -- 서비스 정보 설정 ├── index.html ├── package.json ├── tsconfig.json └── vite.config.ts ``` **다음 단계** - **인증 가드 추가하기** [쉬움] — 로그인하지 않은 사용자가 대시보드에 직접 접근하면 첫 화면으로 돌려보내기 - **에러 처리 개선** [보통] — 로그인 실패나 토큰 갱신 실패 시 사용자에게 안내 메시지 보여주기 - **토큰 자동 갱신** [어려움] — API 호출 시 토큰이 만료되었으면 자동으로 새 토큰을 발급받아 재시도하기 --- ## Next.js로 시작하기 URL: https://myiam.io/docs/quickstart/nextjs Next.js 앱에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. 샘플 코드: https://github.com/myiam-io/nextjs-samples # Next.js로 시작하기 이 가이드는 Next.js, App Router, TypeScript 환경 기준입니다. Next.js 앱에 로그인, 회원가입, 로그아웃 기능을 추가하는 방법을 안내합니다. 처음이라도 코드를 복사해서 붙여넣기만 하면 동작하는 샘플 앱을 만들 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - **Node.js 20 이상**이 설치되어 있어야 합니다. 터미널에서 `node -v`로 확인할 수 있습니다. ### Step 1: 프로젝트 생성 터미널을 열고 아래 명령어를 실행하면 `my-app`이라는 새 프로젝트 폴더가 만들어집니다. 이 가이드의 파일 경로가 모두 `src/` 기준이므로 `--src-dir`을 함께 넘깁니다. `터미널` ```bash npx create-next-app@latest my-app --ts --app --src-dir cd my-app ``` ### Step 2: SDK 설치 MyIAM Web SDK를 설치합니다. `터미널` ```bash npm install @myiam.io/web-sdk ``` - `@myiam.io/web-sdk` — MyIAM 인증 기능을 사용하기 위한 SDK ### Step 3: 환경 변수 설정 프로젝트 최상위 폴더에 `.env` 파일을 만들고, 내 서비스 정보를 입력합니다. 이 값들은 MyIAM 관리 화면에서 확인할 수 있습니다. `.env` 파일은 서버에서만 읽히므로 브라우저에 노출되지 않습니다. - `SERVICE_UID` = `` 서비스 설정 페이지에서 확인할 수 있는 서비스 고유 식별자입니다. - `OAUTH2_CLIENT_ID` = `` OAuth2 설정 페이지에서 확인할 수 있는 클라이언트 식별자입니다. - `API_KEY` = `` 서비스 설정에서 API 키를 생성하면 확인할 수 있습니다. 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. - `COOKIE_SECRET` = `` 세션 암호화에 사용되는 비밀 키입니다. 32자 이상의 랜덤 문자열을 직접 생성하여 입력하세요. `.env` ```env SERVICE_UID= OAUTH2_CLIENT_ID= COOKIE_SECRET= API_KEY= ``` ### Step 4: SDK 초기화 SDK에 내 서비스 정보를 알려주는 파일을 만듭니다. 다른 파일에서 이 파일을 가져다 쓰면 됩니다. `src/lib/myiam.ts` ```typescript import { createMyiamServer } from '@myiam.io/web-sdk/server' // 서버에서 사용할 MyIAM 인스턴스를 만듭니다 // 앱 전체에서 이 파일을 import하여 사용합니다 export const myiam = createMyiamServer({ serviceUid: process.env.SERVICE_UID!, oauth2ClientId: process.env.OAUTH2_CLIENT_ID!, cookieSecret: process.env.COOKIE_SECRET!, apiKey: process.env.API_KEY!, }) ``` ### Step 5: 로그인 / 회원가입 API 링크를 클릭하면 MyIAM 로그인 화면으로 이동하는 API를 만듭니다. 주소 뒤에 `?type=signup`을 붙이면 회원가입 화면이 열립니다. > **설정 필요** > > 아래 개발용 콜백 주소를 **Redirect Url**에 등록하세요. 배포 시에는 실제 도메인으로 변경합니다. > URL: `http://localhost:3000/api/auth/callback` `src/app/api/auth/login/route.ts` ```typescript import { myiam } from '@/lib/myiam' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { type NextRequest } from 'next/server' export async function GET(request: NextRequest) { // 로그인이 끝나면 돌아올 주소 const callbackUrl = `${request.nextUrl.origin}/api/auth/callback` const cookieStore = await cookies() // ?type=signup이면 회원가입, 아니면 로그인 const type = request.nextUrl.searchParams.get('type') const url = type === 'signup' ? await myiam.signup(callbackUrl, cookieStore) : await myiam.login(callbackUrl, cookieStore) // MyIAM 로그인 화면으로 이동 redirect(url) } ``` **회원가입은 로그인과 같은 콜백으로 끝납니다.** MyIAM은 가입이 끝나면 같은 인증 요청을 그대로 이어받아 자동으로 로그인시키고 `/api/auth/callback`으로 돌려보냅니다. 회원가입 링크에 별도의 콜백을 만들 필요가 없습니다. 가입을 마쳤는데 로그인 화면이 한 번 더 뜬다면 서비스 > 서비스 정보 페이지의 회원가입 설정에서 **자동 로그인**이 꺼져 있는 것입니다 (기본값 ON). 꺼져 있으면 방금 만든 계정으로 사용자가 직접 로그인해야 합니다. ### Step 6: 콜백 API 사용자가 로그인을 마치면 MyIAM 서버가 이 주소로 되돌려 보냅니다. 여기서 로그인 결과를 받아 세션 쿠키에 저장하고, 대시보드로 이동합니다. `src/app/api/auth/callback/route.ts` ```typescript import { myiam } from '@/lib/myiam' import { MyiamCallbackError } from '@myiam.io/web-sdk/server' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { type NextRequest } from 'next/server' export async function GET(request: NextRequest) { try { // MyIAM 서버에서 받은 인증 결과를 처리하고, 세션 쿠키에 저장합니다 await myiam.handleCallback(request.nextUrl.searchParams, await cookies()) } catch (err) { // 실패 이유를 첫 화면에 넘겨 사용자가 다시 시도할 수 있게 합니다 if (err instanceof MyiamCallbackError) { console.error('[myiam] callback failed:', err.code, err.message) redirect(`/?error=${err.code}`) } throw err } // 대시보드로 이동 (try 밖이어야 합니다 — 아래 설명 참고) redirect('/dashboard') } ``` **성공 시 `redirect()`는 반드시 `try` 밖에 두세요.** Next.js의 `redirect()`는 내부적으로 예외를 던져 동작하므로, `try` 안에 있으면 바로 아래 `catch`가 그 예외를 삼켜 대시보드로 넘어가지 못합니다. `handleCallback()`이 던지는 `MyiamCallbackError`의 `code`로 실패 원인이 갈립니다. 전체 목록은 [Server SDK의 handleCallback](https://myiam.io/docs/sdk/web/server)에 있고, 개발 중에 가장 자주 만나는 둘은 이렇습니다. | `code` | 원인 | | --- | --- | | `missing_pkce_state` | 로그인을 시작한 지 30분이 지났거나, 다른 탭에서 로그인을 다시 시작해 PKCE 쿠키가 덮어써졌습니다. 다시 로그인하면 됩니다 | | `authorization_error` | 인증 서버가 요청을 거절했습니다. 대부분 **Redirect Url 등록값과 앱이 보낸 주소가 한 글자라도 다른 경우**입니다 | ### Step 7: 로그아웃 API 저장된 세션을 삭제하고 MyIAM 로그아웃 화면으로 이동합니다. > **설정 필요** > > 아래 개발용 주소를 **Logout Redirect Url**에 등록하세요. 배포 시에는 실제 도메인으로 변경합니다. > URL: `http://localhost:3000` `src/app/api/auth/logout/route.ts` ```typescript import { myiam } from '@/lib/myiam' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { type NextRequest } from 'next/server' export async function GET(request: NextRequest) { // 세션 쿠키를 삭제하고 로그아웃 URL을 만듭니다 const url = myiam.logout(request.nextUrl.origin, await cookies()) // MyIAM 로그아웃 화면으로 이동 redirect(url) } ``` | 경로 | 하는 일 | | --- | --- | | `/api/auth/login` | 로그인 화면으로 이동 | | `/api/auth/login?type=signup` | 회원가입 화면으로 이동 | | `/api/auth/callback` | 로그인 완료 후 자동으로 거쳐가는 중간 주소 | | `/api/auth/logout` | 로그아웃 | ### Step 8: 미들웨어로 인증 보호 로그인하지 않은 사용자가 대시보드에 직접 접근하면 첫 화면으로 돌려보내는 역할입니다. `matcher`에 보호할 경로를 추가하면 됩니다. `src/middleware.ts` ```typescript import { myiam } from '@/lib/myiam' import { type NextRequest, NextResponse } from 'next/server' export async function middleware(request: NextRequest) { // 세션 쿠키에서 로그인 정보를 확인합니다 const session = await myiam.getSession(request.cookies) // 로그인하지 않았으면 첫 화면으로 돌려보냅니다 if (!session) { return NextResponse.redirect(new URL('/', request.url)) } return NextResponse.next() } // 보호할 경로를 여기에 추가하세요 export const config = { matcher: ['/dashboard/:path*'], } ``` ### Step 9: 홈 페이지 로그인과 회원가입 링크가 있는 첫 화면입니다. 링크를 클릭하면 위에서 만든 인증 API가 호출됩니다. `src/app/page.tsx` ```react export default async function Home({ searchParams, }: { searchParams: Promise<{ error?: string }> }) { // 콜백이 실패하면 이 화면으로 error 코드를 달고 돌아옵니다 const { error } = await searchParams return (

MyIAM Next.js

{error &&

로그인에 실패했습니다 ({error}). 다시 시도해 주세요.

} 로그인 {' | '} 회원가입
) } ``` ### Step 10: 대시보드 페이지 로그인에 성공하면 보여줄 화면입니다. 미들웨어가 인증을 보장하므로, 이 페이지에 도달했다면 세션은 항상 존재합니다. `src/app/dashboard/page.tsx` ```react import { myiam } from '@/lib/myiam' import { cookies } from 'next/headers' import TokenInfo from './token-info' export default async function Dashboard() { // 미들웨어가 인증을 보장하므로, 세션은 항상 존재합니다 const session = (await myiam.getSession(await cookies()))! return (

대시보드

Access Token: {session.accessToken.slice(0, 20)}...

세션 만료: {new Date(session.expiresAt).toLocaleString('ko-KR')}


로그아웃
) } ``` ### Step 11: HTTP API 호출하기 대시보드에서 MyIAM API를 호출하는 기능을 추가해 봅니다. 서버에서 API를 호출하고, 그 결과를 버튼으로 확인할 수 있습니다. ### API 라우트 서버에서 토큰 정보를 조회합니다. 토큰이 만료되었으면 자동으로 새 세션을 발급받아 다시 시도합니다. `src/app/api/token/info/route.ts` ```typescript import { myiam } from '@/lib/myiam' import { MyiamApiError } from '@myiam.io/web-sdk/server' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' export async function GET() { const cookieStore = await cookies() let session = await myiam.getSession(cookieStore) // 로그인하지 않았으면 에러를 반환합니다 if (!session) { return NextResponse.json({ error: '인증이 필요합니다.' }, { status: 401 }) } try { // MyIAM API로 토큰 정보를 조회합니다 return NextResponse.json(await myiam.api.getTokenInfo(session.accessToken)) } catch (err) { // 토큰이 만료되었으면 세션을 갱신하고 다시 시도합니다 if (err instanceof MyiamApiError && err.status === 401) { session = await myiam.refreshSession(cookieStore) if (!session) { return NextResponse.json({ error: '세션 만료' }, { status: 401 }) } return NextResponse.json(await myiam.api.getTokenInfo(session.accessToken)) } throw err } } ``` ### 클라이언트 컴포넌트 버튼을 눌러 위에서 만든 API를 호출합니다. `src/app/dashboard/token-info.tsx` ```react 'use client' import { useState } from 'react' export default function TokenInfo() { const [result, setResult] = useState('') async function fetchTokenInfo() { setResult('요청 중...') // 위에서 만든 API를 호출합니다 const res = await fetch('/api/token/info') const text = await res.text() try { setResult(JSON.stringify(JSON.parse(text), null, 2)) } catch { setResult(text) } } return (
{result &&
{result}
}
) } ``` ### Step 12: 실행 모든 파일을 저장하고 아래 명령어로 개발 서버를 시작합니다. `터미널` ```bash npm run dev ``` 브라우저에서 `http://localhost:3000`을 열면 로그인 링크가 보입니다. 링크를 클릭해서 로그인이 잘 되는지 확인해 보세요! **전체 인증 흐름** ```mermaid flowchart TD A["로그인 링크 클릭"] A --> B["MyIAM 로그인 화면으로 이동
SDK가 안전한 인증 URL을 만들어 줍니다"] B --> C["사용자가 로그인 완료
MyIAM 서버에서 아이디/비밀번호를 확인합니다"] C --> D["우리 앱의 /api/auth/callback 으로 돌아옴"] D --> E["세션 쿠키 저장
SDK가 인증 결과를 암호화하여 쿠키에 저장합니다"] E --> F["대시보드 진입
미들웨어가 세션을 확인하고 접근을 허용합니다"] F --> G["토큰 정보 조회
서버에서 MyIAM API를 호출합니다"] H["회원가입 링크 클릭"] H --> I["MyIAM 회원가입 화면으로 이동
약관 동의 → 정보 입력 순서로 진행됩니다"] I --> J["가입 완료 후 자동 로그인
서비스 정보의 자동 로그인 설정이 ON일 때 (기본값)"] J --> K["같은 /api/auth/callback 으로 돌아옴
여기서부터는 로그인과 완전히 같습니다"] L["로그아웃 링크 클릭"] L --> M["/api/auth/logout 호출
세션 쿠키를 삭제합니다"] M --> N["MyIAM 로그아웃 화면으로 이동
MyIAM 서버에서 세션을 정리합니다"] N --> O["설정한 Logout Redirect Url로 돌아옴
로그아웃 완료! 첫 화면으로 이동합니다"] ``` **프로젝트 구조** ``` my-app/ ├── src/ │ ├── lib/ │ │ └── myiam.ts -- SDK 초기화 │ ├── middleware.ts -- 인증 보호 │ └── app/ │ ├── page.tsx -- 홈 (로그인/회원가입 링크) │ ├── dashboard/ │ │ ├── page.tsx -- 대시보드 (Server Component) │ │ └── token-info.tsx -- 토큰 조회 (Client Component) │ └── api/ │ ├── auth/ │ │ ├── login/route.ts -- 로그인/회원가입 │ │ ├── callback/route.ts -- 로그인 완료 처리 │ │ └── logout/route.ts -- 로그아웃 │ └── token/ │ └── info/route.ts -- API 호출 예시 ├── .env -- 서비스 정보 설정 (서버 전용) ├── package.json ├── tsconfig.json └── next.config.ts ``` **다음 단계** - **보호 경로 확장** [쉬움] — 미들웨어의 matcher에 보호할 경로를 추가하기 - **에러 처리 개선** [보통] — 로그인 실패나 세션 만료 시 사용자에게 안내 메시지 보여주기 - **토큰 자동 갱신** [어려움] — API 호출 시 토큰이 만료되었으면 refreshSession()으로 자동 갱신 후 재시도하기 --- ## Flutter로 시작하기 URL: https://myiam.io/docs/quickstart/flutter Flutter 앱에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. 샘플 코드: https://github.com/myiam-io/flutter-samples # Flutter로 시작하기 이 가이드는 Flutter, Dart 환경 기준입니다. Flutter 앱에 로그인, 회원가입, 로그아웃 기능을 추가하는 방법을 안내합니다. 처음이라도 코드를 복사해서 붙여넣기만 하면 동작하는 샘플 앱을 만들 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - **Flutter SDK 3.24 이상**이 설치되어 있어야 합니다. 터미널에서 `flutter --version`으로 확인할 수 있습니다. ### Step 1: 프로젝트 생성 터미널을 열고 아래 명령어를 실행하면 `my_app`이라는 새 프로젝트 폴더가 만들어집니다. `터미널` ```bash flutter create my_app cd my_app ``` ### Step 2: SDK 설치 `pubspec.yaml`의 `dependencies`에 아래 패키지를 추가합니다. `pubspec.yaml` ```yaml dependencies: flutter: sdk: flutter flutter_riverpod: ^3.3.1 myiam_flutter_sdk: ^0.8.1 ``` - `myiam_flutter_sdk` — MyIAM 인증 기능을 사용하기 위한 SDK - `flutter_riverpod` — 인증 상태 관리를 위한 라이브러리 `터미널` ```bash flutter pub get ``` ### Step 3: 딥링크 설정 OAuth2 인증 후 앱으로 돌아오기 위해 딥링크를 설정합니다. Android와 iOS 각각 설정이 필요합니다. > **설정 필요** > > 아래 딥링크 주소를 **Redirect Url**에 등록하세요. 앱의 URL Scheme을 변경했다면 그에 맞게 수정합니다. > URL: `myiamsample://oauth2callback` **Android** `android/app/src/main/AndroidManifest.xml`의 `` 안에 intent filter를 추가합니다. `AndroidManifest.xml` ```xml ``` **iOS** `ios/Runner/Info.plist`의 `` 안에 URL Scheme을 추가합니다. `Info.plist` ```xml CFBundleURLTypes CFBundleURLSchemes myiamsample ``` `myiamsample`은 예시입니다. 앱에 맞는 고유한 스킴을 사용하세요. ### Step 4: 환경 변수 설정 프로젝트 최상위 폴더에 `env.json` 파일을 만들고, 내 서비스 정보를 입력합니다. 이 값들은 MyIAM 관리 화면에서 확인할 수 있습니다. - `SERVICE_UID` = `` 서비스 설정 페이지에서 확인할 수 있는 서비스 고유 식별자입니다. - `OAUTH2_CLIENT_ID` = `` OAuth2 설정 페이지에서 확인할 수 있는 클라이언트 식별자입니다. - `API_KEY` = `` 서비스 설정에서 API 키를 생성하면 확인할 수 있습니다. 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. `env.json` ```json { "SERVICE_UID": "", "OAUTH2_CLIENT_ID": "", "API_KEY": "", "REDIRECT_SCHEME": "myiamsample", "REDIRECT_HOST": "oauth2callback" } ``` `env.json`은 `.gitignore`에 추가하여 저장소에 포함되지 않도록 합니다. `REDIRECT_SCHEME`과 `REDIRECT_HOST`는 [딥링크 설정(3단계)](#step-3)과 일치해야 합니다. ### Step 5: SDK 초기화 `lib/main.dart`에서 Riverpod의 `ProviderScope`를 통해 SDK를 초기화합니다. `String.fromEnvironment()`로 `env.json`의 값을 읽습니다. `lib/main.dart` ```dart // lib/main.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; import 'screens/home_screen.dart'; import 'screens/dashboard_screen.dart'; void main() { runApp( ProviderScope( overrides: [ authConfigProvider.overrideWithValue( const MyiamConfig( serviceUid: String.fromEnvironment('SERVICE_UID'), oauth2ClientId: String.fromEnvironment('OAUTH2_CLIENT_ID'), apiKey: String.fromEnvironment('API_KEY'), redirectScheme: String.fromEnvironment('REDIRECT_SCHEME'), redirectHost: String.fromEnvironment('REDIRECT_HOST'), ), ), ], child: const MyApp(), ), ); } ``` `MyApp` 위젯에서 인증 상태에 따라 화면을 전환합니다. `lib/main.dart (계속)` ```dart class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authNotifierProvider); return MaterialApp( title: 'MyIAM Flutter Quickstart', theme: ThemeData( colorSchemeSeed: Colors.indigo, useMaterial3: true, ), home: switch (authState) { AuthStateLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), AuthStateAuthenticated() => const DashboardScreen(), AuthStateUnauthenticated() => const HomeScreen(), AuthStateError() => const HomeScreen(), }, ); } } ``` `authNotifierProvider`가 인증 상태를 자동으로 관리합니다. 로그인/로그아웃 시 화면이 자동 전환됩니다. ### Step 6: 홈 화면 — 로그인 / 회원가입 SDK가 제공하는 `LoginScreen`과 `SignupScreen` 위젯을 사용합니다. 인증이 완료되면 콜백에서 토큰을 받아 사용자 정보를 조회합니다. `lib/screens/home_screen.dart` ```dart // lib/screens/home_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); Future _fetchUser(WidgetRef ref, MyiamTokens tokens) async { final api = ref.read(myiamApiProvider); final userMe = await api.getUser(accessToken: tokens.accessToken); return MyiamUser( uid: userMe.uid, serviceUid: userMe.serviceUid, username: userMe.username, authority: userMe.authority, ); } @override Widget build(BuildContext context, WidgetRef ref) { ref.listen(authNotifierProvider, (previous, next) { if (next is AuthStateAuthenticated) { Navigator.of(context).popUntil((route) => route.isFirst); } }); return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'MyIAM Flutter', style: Theme.of(context).textTheme.headlineLarge, ), const SizedBox(height: 48), FilledButton.icon( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => LoginScreen( onLogin: (tokens) => _fetchUser(ref, tokens), ), ), ); }, icon: const Icon(Icons.login), label: const Text('로그인'), ), const SizedBox(height: 16), OutlinedButton.icon( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => SignupScreen( onSignup: (tokens) => _fetchUser(ref, tokens), ), ), ); }, icon: const Icon(Icons.person_add), label: const Text('회원가입'), ), ], ), ), ); } } ``` `LoginScreen`은 기본 모드인 `customTab`(iOS `ASWebAuthenticationSession` / Android Chrome Custom Tabs)으로 MyIAM 인증 화면을 표시합니다. OAuth2 PKCE, 콜백 처리, 토큰 교환이 모두 SDK 내부에서 처리됩니다. `mode: MyiamWebViewMode.inAppWebView`로 앱 내장 WebView를 쓸 수도 있지만, **로그인에는 권장하지 않습니다.** 내장 WebView에는 WebAuthn이 없어 패스키가 동작하지 않고, 소셜 로그인 제공사는 embedded webview를 차단합니다. 두 기능을 쓰지 않는 서비스에서만 선택하세요. ### Step 7: 대시보드 — 토큰 표시, 갱신, 로그아웃 인증된 사용자에게 정보와 토큰을 보여주고, 토큰 갱신 및 로그아웃 기능을 제공합니다. > **설정 필요** > > 아래 딥링크 주소를 **Logout Redirect Url**에 등록하세요. 앱의 URL Scheme을 변경했다면 그에 맞게 수정합니다. > URL: `myiamsample://oauth2callback` `lib/screens/dashboard_screen.dart` ```dart // lib/screens/dashboard_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authNotifierProvider); ref.listen(authNotifierProvider, (previous, next) { if (next is AuthStateUnauthenticated) { Navigator.of(context).popUntil((route) => route.isFirst); } }); return authState is! AuthStateAuthenticated ? const Scaffold(body: Center(child: CircularProgressIndicator())) : Scaffold( appBar: AppBar( title: const Text('대시보드'), actions: [ TextButton.icon( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const LogoutScreen(), ), ); }, icon: const Icon(Icons.logout), label: const Text('로그아웃'), ), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ Text('사용자: ${authState.user.username}'), Text('UID: ${authState.user.uid}'), if (authState.tokens.expiresIn != null) Text('토큰 만료: ${authState.tokens.expiresIn}초 후'), const Divider(), Text('Access Token: ${authState.tokens.accessToken.substring(0, 20)}...'), Text('Refresh Token: ${authState.tokens.refreshToken.substring(0, 20)}...'), const SizedBox(height: 16), FilledButton.icon( onPressed: () async { final messenger = ScaffoldMessenger.of(context); try { await ref .read(authNotifierProvider.notifier) .refreshToken(); messenger.showSnackBar( const SnackBar(content: Text('토큰이 갱신되었습니다')), ); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('토큰 갱신 실패: $e')), ); } }, icon: const Icon(Icons.refresh), label: const Text('토큰 갱신'), ), ], ), ); } } ``` ### Step 8: 실행 모든 파일을 저장하고 아래 명령어로 앱을 실행합니다. `--dart-define-from-file` 옵션으로 `env.json`의 환경 변수를 앱에 전달합니다. `터미널` ```bash flutter pub get flutter run --dart-define-from-file=env.json ``` 에뮬레이터 또는 실제 기기에서 앱이 실행됩니다. 로그인 버튼을 탭해서 로그인이 잘 되는지 확인해 보세요! **전체 인증 흐름** ```mermaid flowchart TD A["로그인 버튼 탭"] A --> B["LoginScreen (시스템 브라우저) 열림
SDK가 안전한 인증 URL을 만들어 Custom Tab에 표시합니다"] B --> C["사용자가 로그인 완료
MyIAM 서버에서 아이디/비밀번호를 확인합니다"] C --> D["SDK가 딥링크로 콜백 수신
OAuth2 PKCE, 콜백 처리, 토큰 교환이 SDK 내부에서 처리됩니다"] D --> E["사용자 정보 조회
onLogin 콜백에서 토큰을 받아 사용자 정보를 조회합니다"] E --> F["대시보드 진입
authNotifierProvider 상태가 Authenticated로 변경되어 자동 전환"] G["로그아웃 버튼 탭"] G --> H["LogoutScreen에서 세션 정리
SDK가 MyIAM 서버에 로그아웃을 요청합니다"] H --> I["홈 화면으로 복귀
authNotifierProvider 상태가 Unauthenticated로 변경"] ``` **프로젝트 구조** ``` my_app/ ├── lib/ │ ├── main.dart -- SDK 초기화 + 인증 상태 라우팅 │ └── screens/ │ ├── home_screen.dart -- 로그인/회원가입 화면 │ └── dashboard_screen.dart -- 사용자 정보, 토큰, 갱신, 로그아웃 ├── android/ │ └── app/src/main/AndroidManifest.xml -- 딥링크 설정 ├── ios/ │ └── Runner/Info.plist -- URL Scheme 설정 ├── env.json -- 환경 변수 (gitignore 대상) ├── pubspec.yaml -- 의존성 설정 └── analysis_options.yaml ``` **다음 단계** - **인증 가드 추가하기** [쉬움] — AuthGuard 위젯으로 인증되지 않은 사용자가 대시보드에 직접 접근하면 홈 화면으로 돌려보내기 - **에러 처리 개선** [보통] — 로그인 실패나 토큰 갱신 실패 시 사용자에게 SnackBar로 안내 메시지 보여주기 - **사용자 액션 붙이기** [어려움] — MyiamAction으로 프로필 수정, 이메일 변경, 비밀번호 설정, 패스키 등록, 회원 탈퇴 화면 연결하기 --- ## Vue로 시작하기 URL: https://myiam.io/docs/quickstart/vue Vue 앱에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. # Vue로 시작하기 Vue 앱에 MyIAM 인증을 통합하는 가이드입니다. ## 준비 중 Vue SDK와 문서는 현재 준비 중입니다. 곧 제공될 예정입니다. --- ## NestJS로 시작하기 URL: https://myiam.io/docs/quickstart/nestjs NestJS 서버에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. # NestJS로 시작하기 NestJS 백엔드 서버에 MyIAM 인증을 통합하는 가이드입니다. ## 준비 중 NestJS SDK와 문서는 현재 준비 중입니다. 곧 제공될 예정입니다. --- ## Spring Boot로 시작하기 URL: https://myiam.io/docs/quickstart/spring-boot Spring Boot 서버에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. 샘플 코드: https://github.com/myiam-io/springboot-samples # Spring Boot로 시작하기 이 가이드는 Spring Boot, Kotlin 환경 기준입니다. Spring Boot 앱에 로그인, 회원가입, 로그아웃 기능을 추가하는 방법을 안내합니다. MyIAM SDK 없이 Spring Security OAuth2 Client만으로 PKCE 인증을 구현합니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - **JDK 21 이상**이 설치되어 있어야 합니다. 터미널에서 `java -version`으로 확인할 수 있습니다. ### Step 1: 프로젝트 생성 [Spring Initializr](https://start.spring.io)에서 프로젝트를 생성하거나, 아래 파일을 직접 작성합니다. `settings.gradle.kts` ```kotlin rootProject.name = "myiam-quickstart" ``` `build.gradle.kts` ```kotlin plugins { kotlin("jvm") version "2.1.10" kotlin("plugin.spring") version "2.1.10" id("org.springframework.boot") version "3.5.3" id("io.spring.dependency-management") version "1.1.7" } group = "io.myiam.samples" version = "0.0.1" java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } repositories { mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") } kotlin { compilerOptions { freeCompilerArgs.addAll("-Xjsr305=strict") } } ``` - `spring-boot-starter-web` — 웹 서버 기능 - `spring-boot-starter-thymeleaf` — HTML 템플릿 엔진 - `spring-boot-starter-oauth2-client` — OAuth2 인증 (PKCE 자동 처리) ### Step 2: application.yml 설정 `src/main/resources/application.yml`에 MyIAM 연동 정보를 입력합니다. 이 값들은 MyIAM 관리 화면에서 확인할 수 있습니다. - `service-uid` = `` 서비스 설정 페이지에서 확인할 수 있는 서비스 고유 식별자입니다. - `client-id` = `` OAuth2 설정 페이지에서 확인할 수 있는 클라이언트 식별자입니다. - `api-key` = `` 서비스 설정에서 API 키를 생성하면 확인할 수 있습니다. 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. > **설정 필요** > > 아래 개발용 콜백 주소를 **Redirect Url**에 등록하세요. 배포 시에는 실제 도메인으로 변경합니다. > URL: `http://localhost:3000/login/oauth2/code/myiam` `src/main/resources/application.yml` ```yaml server: port: 3000 myiam: base-url: https://api.myiam.io service-uid: api-key: spring: security: oauth2: client: registration: myiam: client-id: client-authentication-method: none authorization-grant-type: authorization_code redirect-uri: http://localhost:3000/login/oauth2/code/myiam scope: openid provider: myiam: authorization-uri: https://app.myiam.io/oauth2/authorize token-uri: https://app.myiam.io/oauth2/token jwk-set-uri: https://app.myiam.io/oauth2/jwks user-name-attribute: sub ``` `client-authentication-method: none`은 퍼블릭 클라이언트를 의미합니다. Spring Security가 자동으로 PKCE(code_verifier/code_challenge)를 처리합니다. ### Step 3: MyIAM 설정 클래스 `application.yml`의 `myiam.*` 값을 코드에서 사용하기 위한 설정 클래스입니다. `MyiamProperties.kt` ```kotlin // src/main/kotlin/io/myiam/samples/quickstart/MyiamProperties.kt package io.myiam.samples.quickstart import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "myiam") data class MyiamProperties(val baseUrl: String, val serviceUid: String, val apiKey: String) ``` `Application.kt` ```kotlin // src/main/kotlin/io/myiam/samples/quickstart/Application.kt package io.myiam.samples.quickstart import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication @SpringBootApplication @EnableConfigurationProperties(MyiamProperties::class) class Application fun main(args: Array) { runApplication(*args) } ``` ### Step 4: Spring Security 설정 이 파일이 MyIAM 연동의 **핵심**입니다. OAuth2 로그인, 회원가입, 로그아웃을 모두 처리합니다. > **설정 필요** > > 아래 개발용 주소를 **Logout Redirect Url**에 등록하세요. 배포 시에는 실제 도메인으로 변경합니다. > URL: `http://localhost:3000` `SecurityConfig.kt` ```kotlin // src/main/kotlin/io/myiam/samples/quickstart/SecurityConfig.kt package io.myiam.samples.quickstart import jakarta.servlet.http.HttpServletRequest import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher @Configuration @EnableWebSecurity class SecurityConfig( private val props: MyiamProperties, private val registrations: ClientRegistrationRepository, ) { @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain = http // 접근 제어: "/" 와 "/error"만 공개, 나머지는 로그인 필요 .authorizeHttpRequests { it.requestMatchers("/", "/error").permitAll().anyRequest().authenticated() } // OAuth2 로그인 설정 .oauth2Login { it.authorizationEndpoint { ae -> ae.authorizationRequestResolver(requestResolver()) } it.defaultSuccessUrl("/dashboard", true) it.failureHandler { req, res, _ -> req.session.invalidate(); res.sendRedirect("/") } } // 로그아웃: MyIAM 로그아웃 화면으로 리디렉션 .logout { it.logoutRequestMatcher( PathPatternRequestMatcher.withDefaults().matcher("/api/auth/logout"), ) it.logoutSuccessHandler { req, res, _ -> // 배포 환경에서는 기본 포트(80/443)에 :포트가 붙지 않도록 한다 val port = req.serverPort val isDefaultPort = (req.scheme == "http" && port == 80) || (req.scheme == "https" && port == 443) val origin = "${req.scheme}://${req.serverName}" + if (isDefaultPort) "" else ":$port" val base = registrations.findByRegistrationId("myiam") .providerDetails.authorizationUri.substringBefore("/oauth2/") res.sendRedirect("$base/logout/form?service_uid=${props.serviceUid}&logout_redirect_uri=$origin") } } .build() // MyIAM의 서비스별 로그인/회원가입 URL로 변환하는 리졸버 private fun requestResolver() = object : OAuth2AuthorizationRequestResolver { private val delegate = DefaultOAuth2AuthorizationRequestResolver(registrations, "/oauth2/authorization") override fun resolve(req: HttpServletRequest) = customize(delegate.resolve(req), req) override fun resolve(req: HttpServletRequest, id: String) = customize(delegate.resolve(req, id), req) private fun customize(authReq: OAuth2AuthorizationRequest?, req: HttpServletRequest) = authReq?.let { val base = it.authorizationUri.substringBefore("/oauth2/") val path = if (req.getParameter("type") == "signup") "signup" else "login" OAuth2AuthorizationRequest.from(it) .authorizationUri("$base/s/${props.serviceUid}/$path") .additionalParameters(it.additionalParameters + ("prompt" to "consent")) .build() } } } ``` | 설정 | 설명 | | --- | --- | | `authorizeHttpRequests` | `/`와 `/error`만 공개, 나머지는 인증 필요 | | `requestResolver()` | 인증 URL을 MyIAM 서비스별 로그인/회원가입 URL로 변환 | | `defaultSuccessUrl` | 로그인 성공 시 `/dashboard`로 이동 | | `logoutSuccessHandler` | MyIAM 로그아웃 화면으로 리디렉션 | `?type=signup` 파라미터로 로그인과 회원가입을 구분합니다. 리졸버가 이 값을 읽어 인증 URL의 경로를 `/s/{serviceUid}/login` 또는 `/s/{serviceUid}/signup`으로 변경합니다. ### Step 5: 컨트롤러 만들기 홈과 대시보드 페이지를 렌더링하는 **페이지 컨트롤러**와, MyIAM REST API를 호출하는 **API 컨트롤러**를 만듭니다. `PageController.kt` ```kotlin // src/main/kotlin/io/myiam/samples/quickstart/PageController.kt package io.myiam.samples.quickstart import org.springframework.security.oauth2.client.OAuth2AuthorizedClient import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping import java.time.ZoneId import java.time.format.DateTimeFormatter @Controller class PageController { @GetMapping("/") fun home() = "index" @GetMapping("/dashboard") fun dashboard(@RegisteredOAuth2AuthorizedClient("myiam") client: OAuth2AuthorizedClient, model: Model): String { model.addAttribute("accessTokenPreview", client.accessToken.tokenValue.take(20) + "...") model.addAttribute("expiresAt", client.accessToken.expiresAt ?.atZone(ZoneId.of("Asia/Seoul")) ?.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) ?: "N/A") return "dashboard" } } ``` `ApiController.kt` ```kotlin // src/main/kotlin/io/myiam/samples/quickstart/ApiController.kt package io.myiam.samples.quickstart import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.oauth2.client.OAuth2AuthorizedClient import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.client.RestClient import org.springframework.web.client.RestClientResponseException @RestController class ApiController(props: MyiamProperties) { private val api = RestClient.builder() .baseUrl(props.baseUrl) .defaultHeader("My-Key", "KEY ${props.apiKey}") .defaultHeader("My-Service", "UID ${props.serviceUid}") .build() // MyIAM API로 토큰 정보를 조회합니다 @GetMapping("/api/token/info") fun tokenInfo(@RegisteredOAuth2AuthorizedClient("myiam") c: OAuth2AuthorizedClient): Any = api.get().uri("/api/v0/token/info") .header("Authorization", "Bearer ${c.accessToken.tokenValue}") .retrieve().body(Any::class.java)!! // 로컬 세션의 OIDC 사용자 정보를 반환합니다 @GetMapping("/api/debug/oidc-user") fun oidcUser(@AuthenticationPrincipal user: OidcUser) = mapOf( "subject" to user.subject, "name" to user.name, "claims" to user.claims, "authorities" to user.authorities.map { it.authority }, ) @ExceptionHandler(RestClientResponseException::class) fun error(e: RestClientResponseException): ResponseEntity = ResponseEntity.status(e.statusCode).body(e.responseBodyAsString) } ``` | 경로 | 역할 | | --- | --- | | `/api/token/info` | MyIAM API로 토큰 정보 조회 | | `/api/debug/oidc-user` | 로컬 세션의 OIDC 사용자 정보 | | `/api/auth/logout` | 로그아웃 (SecurityConfig에서 처리) | ### Step 6: 템플릿 만들기 Thymeleaf 템플릿으로 홈 페이지와 대시보드를 만듭니다. `src/main/resources/templates/index.html` ```html MyIAM Spring Boot

MyIAM Spring Boot

로그인 | 회원가입 ``` `/oauth2/authorization/myiam`은 Spring Security가 자동으로 등록하는 경로입니다. 이 경로로 요청하면 SecurityConfig의 리졸버가 MyIAM 인증 화면으로 리디렉션합니다. `src/main/resources/templates/dashboard.html` ```html 대시보드

대시보드

Access Token:

세션 만료:


    
로그아웃 ``` ### Step 7: 실행 모든 파일을 저장하고 아래 명령어로 개발 서버를 시작합니다. `터미널` ```bash ./gradlew bootRun ``` 브라우저에서 `http://localhost:3000`을 열면 로그인 링크가 보입니다. 링크를 클릭해서 로그인이 잘 되는지 확인해 보세요! **전체 인증 흐름** ```mermaid flowchart TD A["로그인 클릭
/oauth2/authorization/myiam으로 요청"] A --> B["requestResolver()가 인증 URL 변환
/s/{serviceUid}/login 또는 /signup 경로로 변환"] B --> C["MyIAM 인증 화면
사용자가 로그인 또는 회원가입을 완료합니다"] C --> D["Spring Security 토큰 교환
PKCE 검증 + 토큰 교환 + OIDC 세션 생성을 자동으로 처리"] D --> E["대시보드 페이지
OAuth2AuthorizedClient로 토큰에 접근합니다"] E --> F["토큰 정보 조회
RestClient로 MyIAM API를 호출합니다"] G["로그아웃 클릭"] G --> H["MyIAM 로그아웃 화면
세션 무효화 후 MyIAM 서버에서 로그아웃 처리"] H --> I["홈 페이지로 복귀
로그아웃 완료! 설정한 Logout Redirect Url로 돌아옵니다"] ``` **프로젝트 구조** ``` myiam-quickstart/ ├── src/main/ │ ├── kotlin/io/myiam/samples/quickstart/ │ │ ├── Application.kt -- 앱 진입점 │ │ ├── MyiamProperties.kt -- MyIAM 설정 바인딩 │ │ ├── SecurityConfig.kt -- OAuth2 보안 설정 (핵심) │ │ ├── PageController.kt -- 페이지 렌더링 │ │ └── ApiController.kt -- REST API 호출 │ └── resources/ │ ├── application.yml -- 서버 및 OAuth2 설정 │ └── templates/ │ ├── index.html -- 홈 (로그인/회원가입 링크) │ └── dashboard.html -- 대시보드 (토큰 정보, API 호출) ├── build.gradle.kts -- 빌드 설정 및 의존성 ├── settings.gradle.kts -- 프로젝트 이름 └── gradlew ``` **다음 단계** - **프로필 페이지 추가** [쉬움] — OidcUser에서 사용자 정보를 가져와 프로필 페이지를 만들어 보기 - **에러 처리 개선** [보통] — 인증 실패 시 사용자에게 안내 메시지를 보여주고, 에러 페이지 만들기 - **토큰 자동 갱신** [어려움] — OAuth2AuthorizedClientManager를 활용하여 만료된 토큰을 자동으로 갱신하기 --- ## iOS로 시작하기 URL: https://myiam.io/docs/quickstart/ios iOS 앱에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. # iOS로 시작하기 iOS 모바일 앱에 MyIAM 인증을 통합하는 가이드입니다. ## 준비 중 iOS SDK와 문서는 현재 준비 중입니다. 곧 제공될 예정입니다. --- ## Android로 시작하기 URL: https://myiam.io/docs/quickstart/android Android 앱에 MyIAM 인증 기능을 추가하는 방법을 안내합니다. # Android로 시작하기 Android 모바일 앱에 MyIAM 인증을 통합하는 가이드입니다. ## 준비 중 Android SDK와 문서는 현재 준비 중입니다. 곧 제공될 예정입니다. --- ## 관리자 가이드 URL: https://myiam.io/docs/admin/service MyIAM 관리자 콘솔에서 서비스를 설정하고 관리하는 방법을 안내합니다. 서비스 기본 정보, OAuth2, SDK, 로그인 방법 등 주요 설정을 한눈에 확인하세요. 관리자 가이드 # 서비스 관리 서비스의 주요 설정과 연동 정보를 한눈에 확인할 수 있습니다. > **화면 예시** > > 관리자 콘솔 상단 메뉴 예시. "서비스" 메뉴가 강조 표시되어 있습니다. ## [서비스 기본 정보](https://myiam.io/docs/admin/service/service-basic) [서비스 설정](https://myiam.io/docs/admin/service/service-basic#step-1) [로그인 방법 설정](https://myiam.io/docs/admin/service/login-methods) [서비스 약관 설정](https://myiam.io/docs/admin/service/terms) [서비스 정책 설정](https://myiam.io/docs/admin/service/policies) [사용자 정보 설정](https://myiam.io/docs/admin/service/user-profile) [조직 등급 Pro 설정](https://myiam.io/docs/admin/service/service-tier) ## UI 설정 브랜딩서비스 아이콘, 타이틀 테마다크 / 라이트 / 시스템 색상브랜드, 배경, 전경, 테두리 미리보기로그인, 가입, 탈퇴, 수정, 비번, 패스키, 정책, 공지 ## [OAuth2 설정](https://myiam.io/docs/admin/service/oauth2-settings) [Client ID](https://myiam.io/docs/admin/service/oauth2-settings#step-1) [Client Secret](https://myiam.io/docs/admin/service/oauth2-settings#step-1) [Authorization Url](https://myiam.io/docs/admin/service/oauth2-settings#step-1) [Token Url](https://myiam.io/docs/admin/service/oauth2-settings#step-1) [Authorization Grant Type 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-2) [Client Authentication Method 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-2) [Scope 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-2) [Redirect Url 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-2) [Logout Redirect Url 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-2) [Token 설정 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-3) [Client 설정 설정](https://myiam.io/docs/admin/service/oauth2-settings#step-4) ## [API 설정](https://myiam.io/docs/admin/service/api-settings) [서비스 UID](https://myiam.io/docs/admin/service/api-settings#step-1) [해싱된 API 키](https://myiam.io/docs/admin/service/api-settings#step-2) [접근 허용 IP 설정](https://myiam.io/docs/admin/service/api-settings#step-3) --- ## 서비스 기본 설정 URL: https://myiam.io/docs/admin/service/service-basic MyIAM 관리자 콘솔에서 서비스 이름, 아이콘, 로그인 화면 커스터마이징, 회원가입 및 탈퇴 설정을 관리하는 방법을 안내합니다. # 서비스 설정 MyIAM 관리자 콘솔에서 서비스의 기본 정보를 설정하는 방법을 안내합니다. 타이틀, 아이콘, 로그인 화면 커스터마이징, 회원가입 및 탈퇴 설정을 다룹니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. ### Step 1: 서비스 기본 정보 설정 서비스 > 서비스 설정 페이지에서 서비스의 기본 정보를 설정합니다. - **타이틀** — 로그인 화면 상단에 표시되는 서비스 이름입니다. - 다국어 입력기를 통해 여러 언어로 설정할 수 있습니다. - **아이콘** — 로그인 화면 상단에 표시되는 아이콘입니다. - HTML로 SVG 또는 IMG 태그를 작성합니다. - 크기는 40×40으로 설정해야 합니다. - 입력 후 미리보기로 결과를 확인할 수 있습니다. - **테마** — 로그인 화면의 테마를 설정합니다. - DARK, LIGHT, SYSTEM 중 선택할 수 있습니다. - DARK 또는 LIGHT를 선택하면 사용자의 시스템 설정과 관계없이 해당 테마로 고정됩니다. - 미설정 시 기본값은 SYSTEM입니다. 타이틀이 로그인 화면 상단의 서비스 이름으로 표시됩니다. 아이콘이 로그인 화면 상단에 표시됩니다. 테마 설정에 따라 화면이 변경됩니다. ### Step 2: 로그인 화면 Footer 설정 로그인 화면 하단에 표시되는 Footer 영역을 설정합니다. 왼쪽과 오른쪽 각각에 Footer를 설정할 수 있으며, 설정 항목은 동일합니다. - **링크 유형** — Footer 클릭 시 이동할 경로를 선택합니다. - **서비스 정책 경로** — MyIAM 서비스 정책 페이지로 이동합니다. - **외부 링크** — 직접 입력한 외부 URL로 이동합니다. - **타이틀** — 실제 Footer에 표시되는 텍스트입니다. 다국어 입력기로 입력합니다. 왼쪽 Footer 타이틀이 로그인 화면 하단 왼쪽에 표시됩니다. 오른쪽 Footer 타이틀이 로그인 화면 하단 오른쪽에 표시됩니다. ### Step 3: 회원가입 설정 회원가입 관련 설정을 구성합니다. 가입 페이지와 가입 처리 두 영역으로 나뉩니다. - **가입 페이지** - **커스텀 서비스 가입 URL** — 설정하면 로그인 화면의 "회원가입" 링크가 해당 URL로 변경됩니다. 미설정 시 MyIAM 기본 회원가입 페이지가 사용됩니다. - **가입 처리** - **자동 로그인** — 회원가입 완료 후 자동으로 로그인합니다. 미설정 시 기본값은 ON입니다. - **수동 가입 준비** — 서비스에서 가입 준비 API를 호출해야 가입 절차가 시작됩니다. - **수동 가입 준비 콜백 URL** — 수동 가입 준비 완료 후 이동할 URL입니다. - **수동 가입** — 서비스에서 가입 완료 API를 호출해야 가입이 완료됩니다. - **수동 가입 콜백 URL** — 수동 가입 완료 후 이동할 URL입니다. - **프로필 정보 제공** — 회원가입 API 호출 결과로 프로필 정보를 제공합니다. 커스텀 서비스 가입 URL을 설정하면 로그인 화면의 회원가입 링크가 설정한 URL로 변경됩니다. ### Step 4: 회원탈퇴 설정 회원탈퇴 화면과 탈퇴 처리 방식을 설정합니다. - **탈퇴 사유** — 사용자가 탈퇴 시 선택할 수 있는 사유 목록을 커스텀합니다. 설정하지 않으면 기본 사유가 표시됩니다. - **경고 문구** — 탈퇴 확인 화면에 표시되는 경고 문구를 커스텀합니다. 설정하지 않으면 기본 문구가 표시됩니다. - **자동 탈퇴** — 사용자의 탈퇴 요청 시 자동으로 탈퇴를 처리합니다. 미설정 시 기본값은 ON입니다. - **기본 탈퇴 페이지** — MyIAM 기본 탈퇴 페이지를 사용합니다. 미설정 시 기본값은 ON입니다. - **탈퇴 콜백 URL** — 탈퇴 완료 후 이동할 URL입니다. 탈퇴 사유를 설정하지 않으면 기본 탈퇴 사유가 표시됩니다. 경고 문구를 설정하지 않으면 기본 경고 문구가 표시됩니다. ## 고급: 회원가입 콜백 확장 가입 폼 전후에 콜백을 설정하여 가입 프로세스를 확장할 수 있습니다. Coming Soon **다음 단계** - **OAuth2 설정** [보통] — OAuth2 인증 관련 설정을 구성합니다. - **API 설정** [쉬움] — API Key, Client Secret 등 API 연동에 필요한 키를 관리합니다. - **로그인 방법 설정** [쉬움] — 소셜 로그인, 패스키 등 다양한 로그인 방법을 설정합니다. --- ## 회원가입 콜백 확장 URL: https://myiam.io/docs/admin/service/signup-callback 회원가입 폼 전후에 콜백 URL을 설정하여 가입 프로세스를 확장하는 방법을 안내합니다. # 회원가입 콜백 확장 회원가입 폼 전후에 콜백 URL을 설정하여 가입 프로세스를 확장하는 방법을 안내합니다. 외부 서비스 연동이나 추가 검증 단계를 가입 과정에 삽입할 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > 이 설정은 서비스 > 서비스 설정 페이지의 회원가입 설정에서 구성합니다. ### Step 1: 콜백 개념 이해하기 회원가입 콜백은 가입 폼이 표시되기 **전(Pre)** 또는 가입이 완료된 **후(Post)**에 지정한 URL로 사용자를 리다이렉트합니다. - **Pre 콜백** — 가입 폼 표시 전에 호출됩니다. - 사전 검증, 초대 코드 확인 등에 활용할 수 있습니다. - **Post 콜백** — 가입 완료 후에 호출됩니다. - 외부 시스템에 사용자 등록, 웰컴 이메일 발송 등에 활용할 수 있습니다. ### Step 2: Pre 콜백 설정 가입 폼 표시 전에 호출할 콜백 URL을 설정합니다. - **콜백 URL** — 사용자가 리다이렉트될 URL입니다. - 콜백 URL에서 검증을 완료한 후, MyIAM이 제공하는 리턴 URL로 사용자를 다시 리다이렉트해야 합니다. - 리턴 URL은 콜백 요청의 쿼리 파라미터로 전달됩니다. ### Step 3: Post 콜백 설정 가입 완료 후 호출할 콜백 URL을 설정합니다. - **콜백 URL** — 가입 완료 후 사용자가 리다이렉트될 URL입니다. - 콜백 URL에서 후처리를 완료한 후, MyIAM이 제공하는 리턴 URL로 사용자를 다시 리다이렉트해야 합니다. **다음 단계** - **서비스 기본 설정** [쉬움] — 서비스 이름, 아이콘 등 기본 정보를 설정합니다. - **사용자 정보 설정** [쉬움] — 회원가입 시 수집할 사용자 정보 항목을 설정합니다. --- ## OAuth2 설정 URL: https://myiam.io/docs/admin/service/oauth2-settings MyIAM 서비스의 OAuth2 인증 설정 — Redirect URL, Logout Redirect URL, Scope 등을 관리하는 방법을 안내합니다. # OAuth2 설정 MyIAM 서비스의 OAuth2 인증 설정을 관리하는 방법을 안내합니다. Client 기본 정보, 인가 방식, 토큰, 클라이언트 보안 설정을 다룹니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > 이 설정은 서비스 > OAuth2 설정 페이지에서 구성합니다. 페이지는 **Client 기본 정보**, **OAuth2 설정**, **Token 설정**, **Client 설정** 4개 섹션으로 구성되어 있습니다. ### Step 1: Client 기본 정보 확인 OAuth2 클라이언트의 기본 정보를 확인합니다. - **Client ID** — 클라이언트를 식별하는 고유 값입니다. 복사 아이콘으로 클립보드에 복사할 수 있습니다. - **Client Secret** — 클라이언트의 비밀 키입니다. - 키 아이콘을 클릭하여 생성하거나 재생성할 수 있습니다. - 생성된 값은 **한 번만 표시**됩니다. 안전한 곳에 저장하세요. - 재생성하면 기존 값은 **즉시 무효화**됩니다. - 서버 사이드 인증이 필요한 경우에만 사용합니다. SPA나 모바일 앱에서는 PKCE를 권장합니다. - **Authorization URL** — 사용자 인가 요청 엔드포인트입니다. - **Token URL** — 토큰 발급 엔드포인트입니다. ### Step 2: OAuth2 설정 OAuth2 인가 방식과 연동에 필요한 설정을 구성합니다. - **Authorization Grant Type** — 토큰을 발급받는 인가 방식을 선택합니다. - **authorization_code** — 서버 사이드 애플리케이션에서 사용하는 표준 방식 - **client_credentials** — 서비스 간 통신(M2M)에 사용 - **refresh_token** — Refresh Token으로 새 Access Token 발급 - 여러 방식을 동시에 선택할 수 있습니다. - **Client Authentication Method** — 토큰 엔드포인트에서 클라이언트를 인증하는 방식입니다. - **client_secret_basic** — HTTP Basic 인증으로 Client ID와 Secret 전달 - **client_secret_post** — 요청 본문에 Client ID와 Secret 포함 - **none** — 공개 클라이언트(SPA, 모바일 앱)에서 PKCE와 함께 사용 - **Scope** — 클라이언트가 요청하는 권한 범위입니다. - `openid` — OpenID Connect 기본 Scope - `profile` — 사용자 프로필 정보 - `email` — 이메일 정보 - **Redirect URL** — 인가 완료 후 리다이렉트될 URL입니다. - SDK에서 설정한 콜백 URL과 **정확히 일치**해야 합니다. - 개발 환경 URL도 함께 등록하세요. - 여러 URL을 콤마로 구분하여 등록할 수 있습니다. - **Logout Redirect URL** — 로그아웃 후 리다이렉트될 URL입니다. ### Step 3: Token 설정 토큰의 유효기간과 형식을 설정합니다. **"간편 설정"** 버튼을 클릭하면 토큰 마법사를 통해 프리셋 기반으로 빠르게 구성할 수 있습니다. - **간편 설정 프리셋** — 사용 패턴에 맞는 프리셋을 선택하면 토큰 유효기간이 자동으로 채워집니다. - **보안 우선** — 짧은 토큰 수명으로 보안을 최우선으로 합니다. - **균형** — 보안과 편의 사이의 균형 잡힌 설정입니다. - **편의 우선** — 사용자 재로그인 빈도를 최소화합니다. - **개발/테스트** — 개발 환경에 적합한 넉넉한 설정입니다. 간편 설정은 프리셋을 선택하면 토큰 유효기간이 자동으로 채워지는 마법사입니다. 적용 후에도 각 항목을 직접 수정할 수 있습니다. - **Authorization 코드 TTL** — Authorization Code의 유효 시간입니다. - 설정 범위: 60초 ~ 300초 (기본값: 60초) - **Access 토큰 TTL** — Access Token의 유효 시간입니다. - 설정 범위: 300초 ~ 86,400초 (기본값: 43,200초 = 12시간) - **Refresh 토큰 TTL** — Refresh Token의 유효 시간입니다. - 설정 범위: 3,600초 ~ 8,640,000초 (기본값: 1,296,000초 = 15일) - **Access 토큰 형식** - **reference** — 서버 참조형으로, 토큰 자체에는 정보가 없고 서버에서 조회합니다. (기본값) - **self-contained** — JWT 형식으로, 토큰 자체에 사용자 정보가 포함됩니다. - **Refresh 토큰 재사용** — ON이면 기존 Refresh Token을 재사용하고, OFF이면 매번 새 토큰을 발급합니다. (기본값: ON) - **ID 토큰 서명 알고리즘** — ID Token 서명에 사용되는 알고리즘입니다. (기본값: RS256) ### Step 4: Client 설정 클라이언트의 보안 설정을 구성합니다. 수정하려면 우측의 **"수정"** 버튼을 클릭하세요. - **PKCE 사용 필요** — Authorization Code 요청 시 PKCE를 필수로 요구할지 설정합니다. - SPA, 모바일 앱 등 공개 클라이언트에서 권장됩니다. (기본값: OFF) - **사용자 인가 필요** — 사용자에게 권한 동의 화면을 표시할지 설정합니다. - 자사 서비스에서는 일반적으로 OFF로 설정합니다. (기본값: OFF) - **JWK 주소** — 클라이언트의 공개 키를 제공하는 JWK Set URL입니다. - `private_key_jwt` 인증 방식을 사용할 때 필요합니다. - **토큰 인증 서명 알고리즘** — JWT 기반 Client 인증에 사용되는 서명 알고리즘입니다. - **X.509 인증서 주체 DN** — mTLS Client 인증에 사용되는 인증서의 Subject DN입니다. 바로 설정하러 가기 — Token 설정 · Client 설정 **다음 단계** - **API 설정** [쉬움] — API Key, Client Secret 등 API 연동에 필요한 키를 관리합니다. - **로그인 방법 설정** [쉬움] — 소셜 로그인, 패스키 등 다양한 로그인 방법을 설정합니다. - **서비스 기본 설정** [쉬움] — 서비스 이름, 아이콘 등 기본 정보를 설정합니다. --- ## API 설정 URL: https://myiam.io/docs/admin/service/api-settings API 연동에 필요한 API Key, Client Secret 생성과 OAuth2 Redirect URL 설정 방법을 안내합니다. # API 설정 API 연동에 필요한 Endpoint, Service UID, API Key를 확인하고 관리하는 방법을 안내합니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > API 설정 정보는 서비스 메인 페이지의 **API 설정** 섹션에서 확인할 수 있습니다. ### Step 1: Endpoint 및 Service UID 확인 서비스 메인 페이지의 API 설정 섹션에서 SDK 연동에 필요한 기본 정보를 확인합니다. - **Endpoint** — API 엔드포인트 URL입니다. - SDK 초기화 시 서버 주소로 사용됩니다. - 복사 아이콘으로 클립보드에 복사할 수 있습니다. - **서비스 UID** — 서비스 고유 식별자입니다. - SDK 초기화 시 서비스를 식별하는 데 사용됩니다. - 복사 아이콘으로 클립보드에 복사할 수 있습니다. ### Step 2: API Key 생성 API 설정 섹션에서 키 아이콘을 클릭하여 API Key를 생성합니다. 서버 사이드에서 MyIAM API를 호출할 때 인증에 사용됩니다. - 생성된 API Key는 **한 번만 표시**됩니다. - 안전한 곳에 저장하세요. - 재생성하면 기존 키는 **즉시 무효화**됩니다. - 서비스 메인에는 **해시된 키(Hashed API Key)**만 표시됩니다. - 원본 키는 생성 시점에만 확인할 수 있으며, 이후에는 해시 값만 조회 가능합니다. ### Step 3: 접근 허용 IP 설정 API 설정 페이지에서 API 접근을 허용할 IP 주소를 관리합니다. - **CIDR 형식**으로 입력합니다. - 예: `192.168.1.0/24`, `10.0.0.1` - 단일 IP 또는 서브넷 범위를 지정할 수 있습니다. - 복수의 IP를 등록할 수 있습니다. - 허용 IP를 설정하지 않으면 **모든 IP에서 접근이 허용**됩니다. **다음 단계** - **OAuth2 설정** [보통] — Redirect URL, Scope 등 OAuth2 인증 설정을 구성합니다. - **React로 시작하기** [쉬움] — React 앱에 MyIAM 인증 기능을 연동합니다. - **Flutter로 시작하기** [쉬움] — Flutter 앱에 MyIAM 인증 기능을 연동합니다. --- ## 조직 등급 URL: https://myiam.io/docs/admin/service/service-tier 조직 등급별 서비스 수, 최대 사용자 수, API 호출 한도를 확인하고 상위 등급으로 올리는 방법을 안내합니다. # 조직 등급 등급은 **조직** 단위로 적용됩니다. 서비스는 자신의 등급을 갖지 않고 소속 조직의 등급을 물려받으며, 조직에 속하지 않은 서비스는 Free 등급으로 동작합니다. ### Step 1: 현재 등급 확인 조직 등급 페이지에서 지금 보고 있는 서비스에 적용된 등급과 한도를 확인할 수 있습니다. - 현재 적용 중인 등급이 **강조 표시**됩니다. - 등급별로 다음 항목의 한도를 비교할 수 있습니다. - **서비스 수** — 한 조직에 묶을 수 있는 서비스(앱)의 수 - **최대 사용자** — 조직 전체에 등록할 수 있는 사용자 수 - **일반 API** — 분당 일반 API 호출 한도 - **인증 API** — 분당 인증 관련 API 호출 한도 ### Step 2: 등급별 비교 | 항목 | Free | Pro | Enterprise | | --- | --- | --- | --- | | 서비스 수 | 1개 | 10개 | 무제한 | | 최대 사용자 | 20,000명 | 100,000명 | 500,000명 | | 일반 API | 600/분 | 3,000/분 | 15,000/분 | | 인증 API | 3,000/분 | 15,000/분 | 75,000/분 | Enterprise 등급의 한도는 기본 제공량이며, 초과분은 별도 협의합니다. 사용자 수 및 API 호출 한도는 변경될 수 있습니다. 등급별 가격과 추가 사용 요금은 [요금 안내](https://myiam.io/pricing) 페이지에서 확인할 수 있습니다. ### Step 3: 등급 올리기 등급은 조직에 붙어 있으므로, 상위 등급을 쓰려면 **조직을 만들고 그 조직에 서비스를 두는** 방식으로 진행합니다. 두 경우로 나뉩니다. - **아직 조직이 없다면** — 우측 상단 프로필 메뉴의 **조직 만들기**에서 조직 이름과 등급(Pro / Enterprise)을 지정해 생성을 요청합니다. 요청이 승인되면 조직 관리의 **서비스 이동**에서 서비스를 그 조직으로 옮깁니다. - **이미 조직이 있다면** — 조직 관리의 조직 정보에서 **등급 변경 요청**을 사용합니다. - 조직 생성과 등급 변경은 모두 MyIAM 관리자의 승인을 거칩니다. 요청 시 결제 정보 칸에 담당자와 예상 사용량을 함께 남겨주시면 빠르게 안내드립니다. - Enterprise 등급은 [도입문의](mailto:contact@myiam.io)로 맞춤 견적을 받을 수 있습니다. **다음 단계** - **API 설정** [쉬움] — API Key, 접근 허용 IP 등 API 연동에 필요한 설정을 관리합니다. - **OAuth2 설정** [보통] — OAuth2 인증 관련 설정을 구성합니다. --- ## 로그인 방법 설정 URL: https://myiam.io/docs/admin/service/login-methods 아이디/비밀번호, 이메일, 패스키, 소셜 로그인 등 다양한 로그인 방법을 추가하는 방법을 안내합니다. # 로그인 방법 설정 서비스에 다양한 로그인 방법을 추가하는 방법을 안내합니다. 아이디/비밀번호, 이메일, 패스키, 소셜 로그인 등을 설정할 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. ### Step 1: 로그인 방법 관리 페이지 열기 서비스 > 로그인 방법 페이지에서 현재 등록된 로그인 방법 목록을 확인하고 관리합니다. - **방법 추가** — 우측 상단의 "방법 추가" 버튼을 클릭하면 추가 가능한 로그인 방법 목록이 표시됩니다. - 이미 사용 중인 방법은 "사용중"으로 표시되며 중복 추가할 수 없습니다. - 추가할 방법을 선택한 뒤 "추가" 버튼을 클릭하면 목록에 추가됩니다. - **순서 변경** — 드래그 앤 드롭으로 로그인 방법의 표시 순서를 변경할 수 있습니다. - 아이디/비밀번호, 이메일, 패스키는 위치가 고정되어 순서를 변경할 수 없습니다. - 소셜 로그인 간의 순서만 변경 가능합니다. - 순서 변경 후 "순서 변경" 버튼을 클릭해야 저장됩니다. - **설명** — 각 로그인 방법의 설명 아이콘을 클릭하면 로그인 화면에서의 위치를 미리볼 수 있습니다. - **삭제** — 각 로그인 방법의 삭제 아이콘을 클릭하여 삭제합니다. - 삭제 시 해당 방법으로 로그인하던 사용자는 로그인하지 못할 수 있습니다. ### Step 2: 아이디/비밀번호 로그인 기본 아이디/비밀번호 방식의 로그인을 설정합니다. 로그인 목록에서 "설정" 아이콘을 클릭하여 세부 항목을 설정할 수 있습니다. - **아이디 설정** - **아이디 길이** — 최소/최대 길이를 슬라이더로 설정합니다. (1~96자) - **이메일 형식** — ON으로 설정하면 아이디를 이메일 형식으로만 입력할 수 있습니다. - **비밀번호 설정** - **비밀번호 길이** — 최소/최대 길이를 슬라이더로 설정합니다. (1~128자) - **요구사항 옵션** — 대문자, 소문자, 숫자, 특수문자 각각 1개 이상 포함 여부를 설정합니다. 아이디/비밀번호 로그인이 로그인 화면 상단에 표시됩니다. ### Step 3: 이메일 로그인 이메일 기반 로그인을 추가합니다. 비밀번호 없이 이메일만으로 인증합니다. - 추가하면 로그인 화면에 "이메일로 시작하기" 링크가 표시됩니다. - 별도의 세부 설정 없이 추가만 하면 바로 사용할 수 있습니다. 이메일 로그인 링크가 로그인 버튼 하단에 표시됩니다. 이메일을 입력하면 6자리 인증번호가 발송됩니다. 이메일로 전달된 인증번호를 입력하면 로그인이 완료됩니다. ### Step 4: 패스키 로그인 패스키(Passkey) 기반 로그인을 추가합니다. 생체 인증, 보안 키 등 FIDO2 표준을 활용한 비밀번호 없는 로그인 방식입니다. - 추가하면 소셜 로그인 영역 상단에 패스키 로그인 버튼이 표시됩니다. - 별도의 세부 설정 없이 추가만 하면 바로 사용할 수 있습니다. - 사용자는 로그인 후 SDK의 패스키 관리 API를 호출하면 패스키 관리 화면으로 진입하여 패스키를 등록하거나 삭제할 수 있습니다. 패스키 로그인 버튼이 소셜 로그인 영역 상단에 표시됩니다. 사용자가 등록한 패스키 목록을 확인하고, 새 패스키를 추가하거나 기존 패스키를 삭제할 수 있습니다. ### Step 5: 소셜 로그인 소셜 로그인 제공자를 추가합니다. 사용자는 기존 소셜 계정으로 간편하게 로그인할 수 있습니다. - **지원 제공자** — Google, Apple, Kakao, Naver를 지원하며, 앞으로 더 많은 제공자가 추가될 예정입니다. - **연동 가이드** — 설정 화면 우측 상단의 "연동 가이드" 링크를 클릭하면 각 제공자의 개발자 문서로 이동합니다. - **OAuth2 Callback URL 등록** — 소셜 로그인 설정 화면 상단에 표시되는 **OAuth2 Callback URL**을 각 제공자의 개발자 콘솔에 등록해야 합니다. - **필수 설정 항목** - **Client ID** — 소셜 제공자가 발급한 클라이언트 식별자 - **Client Secret** — 클라이언트의 비밀 키 - **기본값 제공 항목** — 아래 항목들은 제공자별 기본값이 자동으로 채워집니다. 입력 필드 우측에 **★** 아이콘이 표시되면 기본값이 적용된 상태이며, 값을 변경하면 "기본값으로 설정" 버튼으로 바뀌어 언제든 원래 값으로 되돌릴 수 있습니다. - **Authorization Grant Type** — 토큰 발급 인가 방식 - **Client Authentication Method** — 클라이언트 인증 방식 - **Scopes** — 요청 권한 범위 - **Authorization URI / Token URI** — 인가 및 토큰 엔드포인트 - **User Info URI** — 사용자 정보 조회 엔드포인트 - **Username Attribute Name** — 사용자 식별자 필드명 소셜 로그인 버튼이 하단 영역에 표시됩니다. ## 고급: Apple 로그인 추가 설정 Apple 로그인은 Client ID/Secret 외에 추가 설정이 필요합니다. Apple 로그인을 사용하려면 Apple Developer 계정에서 다음 정보를 추가로 입력해야 합니다. - **Apple Developer Team ID** — Apple Developer 계정의 팀 식별자입니다. - developer.apple.com → 우측 상단 계정 → Membership details에서 확인합니다. - **Apple Private Key ID** — Sign in with Apple용으로 생성한 Private Key의 식별자입니다. - Certificates, Identifiers & Profiles → Keys에서 확인합니다. - **Apple Private Key** — 다운로드한 .p8 파일의 전체 내용입니다. - 키는 생성 시 1회만 다운로드 가능합니다. - `-----BEGIN PRIVATE KEY-----` 헤더와 `-----END PRIVATE KEY-----` 푸터를 포함하여 입력합니다. ## 고급: 소셜 로그인 제공자별 개발자 콘솔 설정 각 소셜 로그인 제공자의 개발자 콘솔에서 앱을 등록하는 방법을 안내합니다. Coming Soon **다음 단계** - **서비스 약관 설정** [쉬움] — 서비스 이용약관과 개인정보처리방침을 설정합니다. - **사용자 정보 설정** [쉬움] — 회원가입 시 수집할 사용자 정보 항목을 설정합니다. - **OAuth2 설정** [보통] — OAuth2 인증 관련 설정을 구성합니다. --- ## 서비스 약관 설정 URL: https://myiam.io/docs/admin/service/terms 서비스 이용약관, 개인정보처리방침 등 약관을 설정하는 방법을 안내합니다. # 서비스 약관 설정 서비스 이용약관, 개인정보처리방침 등 약관을 관리하는 방법을 안내합니다. 약관은 회원가입 시 동의 화면에 표시됩니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. ### Step 1: 서비스 약관 페이지 열기 서비스 > 서비스 약관 페이지에서 현재 등록된 약관 목록을 확인하고 관리합니다. - 각 약관의 **구분**(필수/선택), **상태**(정상/초안/종료), **보기**, **순서**를 확인할 수 있습니다. - 같은 약관을 개정한 것들은 한 덩어리로 묶여, 가장 최근 약관이 맨 위에 오고 그 아래로 **↳** 표시와 함께 이전 개정이 이어집니다. - 수정·삭제·새 약관 준비 등은 우측 **동작** 칸의 **⋯ 메뉴**에 모여 있습니다. 지금 상태에서 할 수 없는 동작은 잠긴 채로 보입니다. - **"이전 버전 보기"**를 켜면 새 약관으로 교체된 지난 약관까지 함께 표시됩니다. 이 상태에서는 순서 변경이 잠깁니다. - 초안 약관에 노출할 본문이 없으면 이름 아래에 **"본문 없음"** 또는 **"본문이 모두 초안"** 경고가 표시됩니다. - 상태를 바꾸면 항목 이름 옆에 **남은 시간(카운트다운)**이 잠시 표시됩니다. 보통 즉시 반영되지만, 캐시 정리가 늦어지면 이 시간까지 이전 내용이 보일 수 있습니다. - 우측 상단의 **"설명"** 버튼을 클릭하면 약관이 동의 화면에서 어떻게 표시되는지 미리볼 수 있습니다. ### Step 2: 약관 추가 우측 상단의 **"약관 추가"** 버튼을 클릭하여 새 약관 항목을 추가합니다. - **약관 제목** — 동의 화면에 표시되는 약관 이름입니다. - 다국어 입력기로 여러 언어로 설정할 수 있습니다. - **필수/선택** — 약관의 동의 유형을 설정합니다. - **필수** — 동의하지 않으면 회원가입을 진행할 수 없습니다. - **선택** — 동의 여부를 사용자가 선택할 수 있습니다. - **전체 동의** — 동의 화면 상단에 "전체 동의" 체크박스가 표시됩니다. - 사용자가 "전체 동의"를 체크하면 필수 약관과 선택 약관이 모두 체크됩니다. 추가한 약관 항목이 회원가입 시 서비스 정책 동의 화면에 표시됩니다. ### Step 3: 약관 상세 내용 관리 약관 목록에서 항목을 클릭하면 하단에 상세 편집 영역이 펼쳐집니다. 약관의 상세 내용을 작성하고 버전을 관리합니다. - **버전 관리** — 버전별로 약관 내용을 관리할 수 있습니다. - 새 버전을 추가하면 이전 버전의 약관 내용은 그대로 유지됩니다. - 정상 상태의 최신 버전이 사용자에게 표시됩니다. - 표현 정비·오탈자 수정은 새 버전을 추가하면 됩니다. 다시 동의받지 않습니다. - 다시 동의받아야 하는 변경이라면 새 버전이 아니라 **새 약관 준비**를 사용하세요. - **내용 작성** — **리치 텍스트 에디터**로 약관 내용을 작성합니다. - 다국어 입력기로 여러 언어로 작성할 수 있습니다. - **본문 버전의 노출 상태** — 언어별 최신 버전 카드에서 버전 하나씩 올리고 내릴 수 있습니다. - **"지금부터 사용"** — 초안 버전을 회원가입 화면에 표시합니다. 같은 약관의 새 본문이라 재동의는 발생하지 않습니다. - **"초안으로 변경"** — 이 본문을 내립니다. 같은 언어에 노출 중인 이전 버전이 있으면 그 버전이 대신 표시됩니다. - 대신 표시할 본문이 없으면 **제목과 내용이 빈 약관**이 노출된다는 경고와 함께 확인 체크가 요구됩니다. 약관 자체를 내리려면 목록의 **"초안으로 변경"**을 사용하세요. - **삭제** — 해당 언어의 본문 버전을 지웁니다. 약관 항목은 그대로 남습니다. - **보기** — 목록의 보기 아이콘을 클릭하면 사용자에게 표시되는 약관 팝업을 미리볼 수 있습니다. 약관 항목을 클릭하면 약관 상세 내용이 팝업으로 표시됩니다. 약관 상세에서 버전별 내용을 관리할 수 있습니다. ### Step 4: 새 약관으로 바꾸고 다시 동의받기 약관 내용이 바뀌어 **기존 사용자에게 다시 동의를 받아야 할 때** 사용합니다. 오탈자 수정처럼 다시 동의받을 필요가 없는 변경은 새 버전 본문 추가로 처리하세요. - **새 약관 준비** — 사용 중인 약관의 **⋯ 메뉴**에서 선택하면 초안 약관이 새로 만들어집니다. - 이 시점에는 기존 약관이 그대로 보이며 사용자에게 영향이 없습니다. - 본문은 복사되지 않으므로 초안에 언어별 본문을 직접 작성해야 합니다. - 이미 초안이거나 지난 약관인 항목은 이 메뉴가 잠깁니다. 사용 중인 약관에서만 시작할 수 있습니다. - **지금부터 사용** — 초안의 **⋯ 메뉴**에서 선택하면 초안이 실제 약관이 되고 기존 약관은 내려갑니다. - 즉시 전체 사용자가 다시 동의해야 하는 상태가 됩니다. 확인 창에서 체크를 해야 진행됩니다. - 본문이 없어도 막히지 않습니다. 확인 창에서 내용이 빈 약관이 된다는 경고만 표시되므로 본문을 먼저 작성하세요. - 이전 약관에 있던 언어의 본문이 새 약관에 없으면 확인 창에서 경고가 표시됩니다. - 내려간 지난 약관은 동의 이력 보존을 위해 남으며 읽기 전용입니다. - 목록에서 **"이전 버전 보기"**를 켜면 내려간 지난 약관을 확인할 수 있습니다. - **발행 취소** — 잘못 발행했을 때 **⋯ 메뉴**에서 선택하면 직전 약관이 다시 노출됩니다. - 발행했던 약관은 초안으로 내려가므로, 고쳐서 다시 발행할 수 있습니다. - 직전 약관의 동의 기록은 그대로여서 다시 받기로 했던 동의도 함께 취소됩니다. - 한 번도 개정하지 않은 약관은 되돌릴 대상이 없어 메뉴가 잠깁니다. 그냥 내리려면 **초안으로 변경**을 쓰세요. ### Step 5: 약관을 초안으로 변경하기 내용에 문제가 있어 **잠깐 감췄다가 고쳐서 다시 올려야 할 때** 사용합니다. 같은 약관을 그대로 쓰는 것이라 다시 동의를 받지 않습니다. - **초안으로 변경** — **⋯ 메뉴**에서 선택하면 약관이 초안 상태가 됩니다. - 회원가입 화면에서 해당 약관이 사라집니다. 필수 약관이라면 동의 절차에서도 빠집니다. - 이미 받아 둔 동의 기록은 그대로 유지되며, 다시 노출해도 재동의는 발생하지 않습니다. - 초안이거나 지난 약관인 항목은 이 메뉴가 잠깁니다. - **다시 노출** — 내용을 고친 뒤 **⋯ 메뉴**의 **"지금부터 사용"**을 선택합니다. - 기존 동의가 그대로 유효하므로 다시 동의받지 않습니다. - 단, 개정을 위해 만들었던 초안(↳ 표시가 붙지 않은 새 약관)은 동의 기록이 없어 노출 시 모든 사용자가 다시 동의해야 합니다. 확인 창에서 경고와 체크가 요구됩니다. - 다시 동의를 받아야 하는 변경이라면 **새 약관 준비**를 사용하세요. ### Step 6: 약관 순서 변경 및 삭제 약관 목록의 표시 순서와 삭제를 관리합니다. - **순서 변경** — 드래그 앤 드롭으로 약관의 표시 순서를 변경합니다. - 동의 화면에서 위에서부터 순서대로 표시됩니다. - 순서 변경 후 **"순서 변경"** 버튼을 클릭해야 저장됩니다. **"순서 취소"**를 누르면 원래 순서로 돌아갑니다. - 개정으로 묶인 지난 약관 행은 따로 옮길 수 없고 계열 전체가 함께 움직입니다. - **삭제** — **⋯ 메뉴**에서 약관을 삭제하며, 확인 창의 체크가 필요합니다. - 삭제된 약관은 회원가입 동의 화면에서 더 이상 표시되지 않습니다. 약관과 본문이 함께 지워집니다. - 종료된 지난 약관은 동의한 사용자가 한 명도 없을 때만 삭제됩니다. 남아 있으면 이력 보존을 위해 삭제가 거부됩니다. **다음 단계** - **서비스 정책 설정** [쉬움] — 서비스 정책 페이지를 설정합니다. - **사용자 정보 설정** [쉬움] — 회원가입 시 수집할 사용자 정보 항목을 설정합니다. - **서비스 기본 설정** [쉬움] — 서비스 이름, 아이콘 등 기본 정보를 설정합니다. --- ## 서비스 정책 설정 URL: https://myiam.io/docs/admin/service/policies 서비스 정책 페이지를 문서형태와 목록형태로 설정하는 방법을 안내합니다. # 서비스 정책 설정 서비스 정책 페이지를 관리하는 방법을 안내합니다. 정책은 문서형태와 목록형태 두 가지로 제공됩니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. ### Step 1: 서비스 정책 페이지 열기 서비스 > 서비스 정책 페이지에서 현재 등록된 정책 목록을 확인하고 관리합니다. - 각 정책의 **구분**(정책형/목록형), **이름과 경로**, **상태**(정상/초안), **순서**, **보기**를 확인할 수 있습니다. - 수정·상태 변경·삭제는 우측 **동작** 칸의 **⋯ 메뉴**에 모여 있습니다. 지금 상태에서 할 수 없는 동작은 잠긴 채로 보입니다. - **"보기"** 아이콘을 클릭하면 실제 정책 페이지가 새 탭에서 열립니다. - 초안 정책에 노출할 본문이 없으면 이름 아래에 **"본문 없음"** 또는 **"본문이 모두 초안"** 경고가 표시됩니다. - 상태를 바꾸면 정책 이름 옆에 **남은 시간(카운트다운)**이 잠시 표시됩니다. 보통 즉시 반영되지만, 캐시 정리가 늦어지면 이 시간까지 이전 내용이 보일 수 있습니다. - 우측 상단의 **"설명"** 버튼을 클릭하면 정책 화면의 구조를 미리볼 수 있습니다. ### Step 2: 정책 유형 이해하기 정책은 두 가지 유형으로 만들 수 있습니다. - **정책형태(문서형)** — 하나의 문서로 된 정책입니다. - 이용약관, 개인정보처리방침 등 단일 문서 형태에 적합합니다. - 리치 텍스트 에디터로 내용을 작성합니다. - **목록형태(게시판형)** — 제목과 등록일이 있는 게시판 형태입니다. - 공지사항, FAQ 등 여러 게시물을 관리하는 형태에 적합합니다. - 게시물을 추가, 수정, 삭제할 수 있습니다. 추가한 정책 항목이 서비스 정책 페이지의 탭으로 표시됩니다. ### Step 3: 정책 추가 우측 상단의 **"정책 추가"** 버튼을 클릭하여 새 정책을 추가합니다. - **정책 제목** — 정책 페이지의 탭에 표시되는 이름입니다. - 다국어 입력기로 여러 언어로 설정할 수 있습니다. - **정책 유형** — 정책형태 또는 목록형태를 선택합니다. - **정책 경로** — 정책 페이지의 URL 경로입니다. - 정책 플랫폼 URL에 경로가 추가되어 접근할 수 있습니다. - 로그인 화면 Footer의 링크 유형을 "서비스 정책 경로"로 설정하면 해당 경로의 정책 페이지로 연결됩니다. ### Step 4: 정책 내용 관리 정책 목록에서 항목을 클릭하면 하단에 상세 편집 영역이 펼쳐집니다. 정책의 상세 내용을 작성하고 관리합니다. - **정책형태** — 리치 텍스트 에디터로 문서를 작성합니다. - 다국어 입력기로 여러 언어로 정책 내용을 작성할 수 있습니다. - **목록형태** — 게시물을 추가하고 관리합니다. - 각 게시물의 제목과 내용을 리치 텍스트 에디터로 작성합니다. - 다국어 입력기로 여러 언어로 작성할 수 있습니다. - **본문 버전의 노출 상태** — 언어별 최신 버전 카드에서 버전 하나씩 올리고 내릴 수 있습니다. - **"지금부터 사용"** — 초안 버전을 정책 페이지에 표시합니다. - **"초안으로 변경"** — 이 본문을 내립니다. 같은 언어에 노출 중인 다른 버전이 있으면 그 버전이 대신 표시됩니다. - 대신 표시할 본문이 없으면 **내용이 빈 정책 페이지**가 된다는 경고와 함께 확인 체크가 요구됩니다. 정책 자체를 내리려면 목록의 **"초안으로 변경"**을 사용하세요. - **삭제** — 해당 언어의 본문 버전을 지웁니다. 정책 항목은 그대로 남습니다. 정책 항목을 클릭하면 정책 상세 내용이 문서 형태로 표시됩니다. ### Step 5: 정책 노출하고 감추기 정책은 **정상**일 때만 서비스 정책 페이지에 표시됩니다. 작성 중이거나 잠시 내려야 하는 정책은 **초안**으로 둡니다. - **지금부터 사용** — 초안 정책의 **⋯ 메뉴**에서 선택하면 정책 페이지에 바로 표시됩니다. - 본문이 없거나 전부 초안이어도 막지 않습니다. 확인 창에서 제목만 있고 내용이 빈 페이지가 된다는 경고가 표시됩니다. - 이미 노출 중인 정책은 이 메뉴가 잠깁니다. - **초안으로 변경** — 노출 중인 정책의 **⋯ 메뉴**에서 선택하면 정책 페이지에서 사라집니다. - 내용을 고친 뒤 **"지금부터 사용"**으로 다시 노출하면 됩니다. - 해당 정책이 **로그인 화면 하단(Footer) 링크**에 연결되어 있으면 경고와 확인 체크가 요구됩니다. 링크는 그대로 남아 누르면 페이지를 찾을 수 없다는 화면이 나오므로, 서비스 > 서비스 설정의 Footer 설정에서 링크를 먼저 바꾸거나 비우세요. ### Step 6: 정책 순서 변경 및 삭제 정책 목록의 표시 순서와 삭제를 관리합니다. - **순서 변경** — 드래그 앤 드롭으로 정책 탭의 표시 순서를 변경합니다. - 순서 변경 후 **"순서 변경 적용"** 버튼을 클릭해야 저장됩니다. **"순서 취소"**를 누르면 원래 순서로 돌아갑니다. - **삭제** — **⋯ 메뉴**에서 정책을 삭제하며, 확인 창의 체크가 필요합니다. - 삭제된 정책은 서비스 정책 페이지에서 더 이상 표시되지 않습니다. - 잠시 내려두는 것이라면 삭제 대신 **초안으로 변경**을 사용하세요. **다음 단계** - **서비스 약관 설정** [쉬움] — 서비스 이용약관과 개인정보처리방침을 설정합니다. - **사용자 정보 설정** [쉬움] — 회원가입 시 수집할 사용자 정보 항목을 설정합니다. - **서비스 기본 설정** [쉬움] — 서비스 이름, 아이콘 등 기본 정보를 설정합니다. --- ## 사용자 정보 설정 URL: https://myiam.io/docs/admin/service/user-profile 회원가입 시 수집할 사용자 정보 항목과 화면 표시 방식을 설정하는 방법을 안내합니다. # 사용자 정보 설정 회원가입 시 수집할 사용자 정보 항목을 설정하는 방법을 안내합니다. 기본 제공 항목의 수집 여부와 표시 방식을 관리할 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. ### Step 1: 사용자 정보 페이지 열기 서비스 > 사용자 정보 페이지에서 사용자 정보 항목을 관리합니다. - 각 항목의 **필수 수집**, **선택 수집**, **수집 안함** 여부를 라디오 버튼으로 설정합니다. - 변경된 항목이 있으면 **"항목 변경"** 버튼을 클릭해야 저장됩니다. - 우측 상단의 **"설명"** 버튼을 클릭하면 가입 화면에서 어떻게 표시되는지 미리볼 수 있습니다. 각 항목의 필수/선택/수집안함을 라디오 버튼으로 설정합니다. 가입 화면 열에서 항목이 속한 화면 번호를 확인할 수 있습니다. ### Step 2: 기본 정보 항목 설정 MyIAM에서 기본 제공하는 사용자 정보 항목을 설정합니다. - **아이디** — 로그인에 사용되는 고유 식별자입니다. (필수) - 아이디/비밀번호 로그인 방법이 등록되어 있을 때 자동으로 표시됩니다. - **비밀번호** — 로그인에 사용되는 비밀번호입니다. (필수) - 아이디/비밀번호 로그인 방법이 등록되어 있을 때 자동으로 표시됩니다. - **이메일** — 이메일 주소입니다. - 필수 수집, 선택 수집, 수집 안함 중 선택합니다. - **이름** — 사용자 이름입니다. - 필수 수집, 선택 수집, 수집 안함 중 선택합니다. ### Step 3: 화면 표시 방식 설정 사용자 정보 입력 화면의 표시 방식을 설정합니다. - **단일 화면** — 모든 정보 항목을 하나의 화면에 표시합니다. - **다중 화면** — 항목을 여러 화면으로 나누어 단계별로 입력합니다. - **가입 화면 나누기** — 특정 항목부터 새로운 화면을 시작합니다. - **가입 화면 합치기** — 현재 화면을 위 화면과 합칩니다. - 목록의 "가입 화면" 열에서 각 항목이 어느 화면에 속하는지 확인할 수 있습니다. 설정한 사용자 정보 항목이 하나의 화면에 모두 표시됩니다. ### Step 4: 항목 순서 변경 사용자 정보 항목의 표시 순서를 드래그 앤 드롭으로 변경할 수 있습니다. - 가입 화면에서 위에서부터 순서대로 표시됩니다. - 드래그로 이동한 항목이 기존 화면 구분과 맞지 않으면 자동으로 조정됩니다. - 순서와 항목 변경이 동시에 있는 경우 **"전체 변경"** 버튼으로 한 번에 저장할 수 있습니다. ## 고급: 커스텀 정보 설정 서비스 고유의 사용자 정보 항목을 추가합니다. 기본 제공 항목 외에 서비스에 필요한 커스텀 정보 항목을 추가할 수 있습니다. 좌측 상단의 **"커스텀 정보 추가"** 버튼을 클릭합니다. 자세한 내용은 [커스텀 정보 설정](https://myiam.io/docs/admin/service/user-profile-custom) 문서를 참고하세요. **다음 단계** - **커스텀 정보 설정** [보통] — 서비스 고유의 사용자 정보 항목을 추가합니다. - **서비스 약관 설정** [쉬움] — 서비스 이용약관과 개인정보처리방침을 설정합니다. - **로그인 방법 설정** [쉬움] — 다양한 로그인 방법을 설정합니다. --- ## 커스텀 정보 설정 URL: https://myiam.io/docs/admin/service/user-profile-custom 기본 제공 항목 외에 서비스 고유의 사용자 정보 항목을 추가하는 방법을 안내합니다. # 커스텀 정보 설정 기본 제공 항목 외에 서비스 고유의 사용자 정보 항목을 추가하는 방법을 안내합니다. 텍스트, 선택, 날짜 등 다양한 입력 유형을 지원합니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > 이 설정은 서비스 > 사용자 정보 페이지에서 구성합니다. ### Step 1: 커스텀 항목 추가 사용자 정보 페이지에서 **"항목 추가"** 버튼을 클릭하여 커스텀 항목을 추가합니다. - **항목 제목** — 입력 폼에 표시되는 라벨입니다. - 다국어 입력기로 여러 언어로 설정할 수 있습니다. - **필수/선택** — 필수 항목은 값을 입력하지 않으면 가입할 수 없습니다. - **입력 유형** — 텍스트, 선택, 날짜 등의 입력 유형을 선택합니다. ### Step 2: 입력 유형 선택 커스텀 항목의 입력 유형을 선택합니다. - **텍스트** — 자유 텍스트 입력입니다. - **긴 텍스트** — 여러 줄의 텍스트 입력입니다. - **선택** — 미리 정의된 옵션 중 하나를 선택합니다. - **날짜** — 날짜 선택기로 날짜를 입력합니다. ### Step 3: 커스텀 항목 수정 기존 커스텀 항목을 클릭하면 상세 편집 화면으로 이동합니다. - 항목 제목, 필수/선택 여부, 입력 유형을 수정할 수 있습니다. - 선택 유형인 경우 옵션 목록을 편집할 수 있습니다. - 더 이상 필요하지 않은 항목은 삭제할 수 있습니다. **다음 단계** - **사용자 정보 설정** [쉬움] — 기본 사용자 정보 항목 설정으로 돌아갑니다. - **서비스 약관 설정** [쉬움] — 서비스 이용약관과 개인정보처리방침을 설정합니다. --- ## Firebase + Flutter 연동 가이드 URL: https://myiam.io/docs/guide/firebase-flutter MyIAM과 Firebase를 함께 사용하는 방법을 안내합니다. Flutter SDK와 Web SDK를 활용한 Custom Token 브릿지 패턴. 샘플 코드: https://github.com/myiam-io/firebase-flutter-sample # Firebase + Flutter 연동 가이드 이 가이드는 Flutter, Dart, Firebase 환경 기준입니다. MyIAM을 인증 제공자로 사용하면서 Firebase 서비스(Firestore, Cloud Functions, Storage)를 함께 활용하는 방법을 안내합니다. MyIAM 로그인 후 자동으로 Firebase Auth에 연결되어 Firebase 서비스를 바로 사용할 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - [Flutter Quickstart](https://myiam.io/docs/quickstart/flutter)를 완료하여 MyIAM 로그인이 동작하는 상태여야 합니다. > - [Firebase 프로젝트](https://console.firebase.google.com/) 생성 및 Flutter 앱 등록이 완료되어야 합니다. > - `flutterfire configure`를 실행하여 `firebase_options.dart`가 생성되어 있어야 합니다. > - 서버 측에 `mintFirebaseToken` Cloud Function이 배포되어 있어야 합니다. (아래 2단계 참고) ## MyIAM 계정과 Firebase 계정의 연결 이 SDK의 핵심은 **MyIAM 사용자와 Firebase Auth 사용자를 1:1로 연결**하는 것입니다. MyIAM이 인증의 단일 소스(Single Source of Truth)이고, Firebase Auth는 Firestore, Storage 등 Firebase 서비스 접근을 위한 브릿지 역할만 수행합니다. ### 왜 계정 연결이 필요한가? Firestore, Cloud Storage 등 Firebase 서비스는 **Firebase Auth 사용자**를 기준으로 접근 제어(Security Rules)를 수행합니다. MyIAM으로 로그인한 사용자가 Firebase 서비스에 접근하려면, MyIAM 계정에 대응하는 Firebase Auth 사용자 세션이 필요합니다. **MyIAM ↔ Firebase 계정 매핑** ```mermaid flowchart LR A["MyIAM 사용자
service_user_uid:
#quot;47DJDkD0Fz...#quot;
username: #quot;john#quot;
email: #quot;j@mail.com#quot;"] A -->|"1:1 매핑 (그대로 사용)"| B["Firebase Auth 사용자
uid:
#quot;47DJDkD0Fz...#quot;
displayName: #quot;john#quot;
email: #quot;j@mail.com#quot;"] ``` 계정 연결은 Cloud Function(`mintFirebaseToken`)에서 자동으로 수행됩니다. 앱 개발자가 별도로 구현할 내용은 없습니다. ### Step 1: SDK 설치 `pubspec.yaml`의 `dependencies`에 아래 패키지를 추가합니다. `pubspec.yaml` ```yaml dependencies: flutter: sdk: flutter flutter_riverpod: ^3.3.1 myiam_flutter_sdk: ^0.7.8 myiam_firebase_flutter_sdk: ^0.1.1 firebase_core: ^4.7.0 firebase_auth: ^6.4.0 cloud_firestore: ^6.0.0 cloud_functions: ^6.2.0 ``` - `myiam_firebase_flutter_sdk` — MyIAM + Firebase 통합 인증 SDK - `firebase_core`, `firebase_auth` — Firebase 핵심 패키지 - `cloud_functions` — Custom Token 교환을 위한 Cloud Functions 클라이언트 `myiam_firebase_flutter_sdk` 0.1.1은 `myiam_flutter_sdk` **0.7.x**를 요구합니다. Flutter Quickstart를 0.8.x로 진행했다면 이 가이드에 맞춰 `^0.7.8`로 내려야 `flutter pub get`이 해결됩니다. `터미널` ```bash flutter pub get ``` ### Step 2: Cloud Function 배포 #### 왜 Custom Token 변환이 필요한가요? MyIAM과 Firebase는 각각 독립된 Identity Provider(IdP)입니다. MyIAM OAuth2 로그인으로 발급받은 access_token은 MyIAM API에서만 유효하며, Firebase Security Rules는 **Firebase Auth가 발급한 ID Token**만 인식합니다. 따라서 MyIAM access_token만으로는 Firestore, Cloud Storage 등 Firebase 서비스에 접근할 수 없습니다. 이 문제를 해결하는 Firebase의 공식 메커니즘이 [Custom Token 인증](https://firebase.google.com/docs/auth/admin/create-custom-tokens)입니다. Custom Token은 Firebase Admin SDK가 서명하는 JWT로, `signInWithCustomToken()`에 전달하면 Firebase Auth가 해당 `uid`로 세션을 생성합니다. 이 과정을 통해 MyIAM의 `service_user_uid`와 Firebase의 `uid`가 1:1로 매핑되므로, Firestore Security Rules에서 `request.auth.uid` 기반 접근 제어가 가능해집니다. 자세한 내용은 [계정 연결](#account-linking) 섹션을 참고하세요. **Custom Token은 반드시 서버 환경(Cloud Function)에서만 발급해야 합니다.** Custom Token 발급에는 Firebase Admin SDK와 서비스 계정 키(Service Account Key)가 필요합니다. 서비스 계정 키가 클라이언트에 노출되면 Firebase 프로젝트 전체가 위험해집니다. **Custom Token 교환 흐름** ```mermaid sequenceDiagram participant app as Flutter 앱 participant fn as Cloud Function participant server as MyIAM 서버 app->>fn: access_token 전달 (mintFirebaseToken 호출) fn->>server: 토큰 검증 요청 (getTokenInfo API 호출) server-->>fn: service_user_uid 반환 (토큰 유효 시 사용자 식별자 응답) Note over fn: Custom Token 발급
createCustomToken(uid) fn-->>app: Custom Token 반환 (서명된 JWT 반환) Note over app: Firebase 로그인
signInWithCustomToken() ``` #### Cloud Function 구현 이 SDK는 MyIAM access_token을 Firebase Custom Token으로 교환하는 Cloud Function이 필요합니다. Cloud Function은 다음을 수행합니다: 1. 클라이언트에서 MyIAM access_token을 받습니다 2. MyIAM API(`getTokenInfo`)로 토큰의 유효성을 검증합니다 3. 검증된 `service_user_uid`로 Firebase Admin SDK의 `createCustomToken()`을 호출합니다 4. 발급된 Custom Token을 클라이언트에 반환합니다 이렇게 하면 MyIAM의 `service_user_uid`가 Firebase의 `uid`와 동일하게 매핑되어, Firestore 보안 규칙에서 `request.auth.uid`로 접근 제어를 할 수 있습니다. 구현 예시는 [firebase-flutter-sample](https://github.com/myiam-io/firebase-flutter-sample)을 참고하세요. Cloud Function 이름은 기본적으로 `mintFirebaseToken`입니다. 다른 이름을 사용하려면 [커스터마이징](#code-custom-function) 섹션을 참고하세요. ### Step 3: Firebase 초기화 `main.dart`에서 기존 MyIAM 초기화 코드에 Firebase 초기화를 추가합니다. `WidgetsFlutterBinding.ensureInitialized()`와 `Firebase.initializeApp()`을 `runApp()` 전에 호출합니다. `lib/main.dart` ```dart // lib/main.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; import 'package:myiam_firebase_flutter_sdk/myiam_firebase_flutter_sdk.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); runApp( ProviderScope( overrides: [ authConfigProvider.overrideWithValue( const MyiamConfig( serviceUid: String.fromEnvironment('SERVICE_UID'), oauth2ClientId: String.fromEnvironment('OAUTH2_CLIENT_ID'), apiKey: String.fromEnvironment('API_KEY'), redirectScheme: String.fromEnvironment('REDIRECT_SCHEME'), redirectHost: String.fromEnvironment('REDIRECT_HOST'), ), ), ], child: const MyApp(), ), ); } ``` ### Step 4: 인증 상태를 Firebase 통합 상태로 전환 기존 `authNotifierProvider` 대신 `firebaseAuthProvider`를 사용하여 MyIAM + Firebase 통합 인증 상태를 관리합니다. `lib/main.dart (계속)` ```dart class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(firebaseAuthProvider); return MaterialApp( title: 'MyIAM Firebase Flutter', theme: ThemeData( colorSchemeSeed: Colors.indigo, useMaterial3: true, ), home: switch (authState) { FirebaseAuthLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), FirebaseAuthAuthenticated() => const DashboardScreen(), FirebaseAuthUnauthenticated() => const HomeScreen(), FirebaseAuthError() => const HomeScreen(), }, ); } } ``` `firebaseAuthProvider`는 내부적으로 `authNotifierProvider`를 감시합니다. MyIAM 로그인이 완료되면 자동으로 Firebase Custom Token 브릿지를 연결합니다. ### Step 5: 대시보드에서 Firebase 연결 상태 확인 `FirebaseAuthAuthenticated` 상태에는 Firebase 연결 정보가 포함됩니다. 연결 상태, 에러 메시지, 토큰 갱신 기능을 대시보드에서 확인할 수 있습니다. `lib/screens/dashboard_screen.dart` ```dart // lib/screens/dashboard_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:myiam_firebase_flutter_sdk/myiam_firebase_flutter_sdk.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(firebaseAuthProvider); return authState is! FirebaseAuthAuthenticated ? const Scaffold(body: Center(child: CircularProgressIndicator())) : Scaffold( appBar: AppBar( title: const Text('대시보드'), actions: [ TextButton.icon( onPressed: () async { await ref.read(firebaseAuthProvider.notifier).logout(); }, icon: const Icon(Icons.logout), label: const Text('로그아웃'), ), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ Text('사용자: ${authState.myiamUser.username}'), Text('UID: ${authState.myiamUser.uid}'), const Divider(), // Firebase 연결 상태 if (authState.firebaseConnected) const Text('Firebase: 연결됨 ✓', style: TextStyle(color: Colors.green)) else if (authState.firebaseError != null) Text('Firebase 오류: ${authState.firebaseError}', style: const TextStyle(color: Colors.red)) else const Text('Firebase: 연결 중...'), const SizedBox(height: 16), FilledButton.icon( onPressed: () async { final messenger = ScaffoldMessenger.of(context); try { await ref .read(firebaseAuthProvider.notifier) .refreshToken(); messenger.showSnackBar( const SnackBar(content: Text('토큰이 갱신되었습니다')), ); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('토큰 갱신 실패: $e')), ); } }, icon: const Icon(Icons.refresh), label: const Text('토큰 갱신'), ), ], ), ); } } ``` ### Step 6: Firestore 사용 예시 Firebase 인증이 완료되면 Firestore에 바로 접근할 수 있습니다. Firebase uid는 MyIAM의 `service_user_uid`와 동일합니다. `Firestore 접근 예시` ```dart import 'package:cloud_firestore/cloud_firestore.dart'; // Firebase 인증이 완료된 상태에서 final doc = await FirebaseFirestore.instance .collection('users') .doc(authState.myiamUser.uid) .get(); ``` Firestore 보안 규칙에서 `request.auth.uid`로 접근 제어를 설정하세요. 이 uid는 MyIAM의 `service_user_uid`와 동일합니다. ### Step 7: 실행 모든 파일을 저장하고 아래 명령어로 앱을 실행합니다. `터미널` ```bash flutter pub get flutter run --dart-define-from-file=env.json ``` 로그인 후 대시보드에서 Firebase 연결 상태가 "연결됨"으로 표시되는지 확인하세요. **전체 인증 흐름** ```mermaid flowchart TD A["MyIAM 로그인 완료
LoginScreen / SignupScreen에서 인증 완료"] A --> B["Firebase 브릿지 자동 연결
firebaseAuthProvider가 MyIAM 인증 상태를 감지합니다"] B --> C["Cloud Function 호출
mintFirebaseToken에 MyIAM access_token을 전송합니다"] C --> D["Firebase Custom Token 수신
Cloud Function이 검증 후 Custom Token을 발급합니다"] D --> E["Firebase 인증 완료
signInWithCustomToken()으로 Firebase Auth 로그인"] E --> F["Firebase 서비스 사용 가능
Firestore, Storage 등 Firebase 서비스에 바로 접근 가능"] G["로그아웃
Firebase signOut() + MyIAM logout() 양쪽 모두 세션 정리"] ``` ## 인증 상태 (FirebaseAuthState) #### 상태 | 속성명 | 설명 | | --- | --- | | FirebaseAuthLoading | 초기 로딩 중 | | FirebaseAuthAuthenticated | MyIAM 인증 완료 (Firebase 연결 진행 중 또는 완료) | | FirebaseAuthUnauthenticated | 미인증 | | FirebaseAuthError | 에러 발생 | #### FirebaseAuthAuthenticated 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | myiamTokens | MyiamTokens | MyIAM access/refresh 토큰 | | myiamUser | MyiamUser | MyIAM 사용자 정보 | | firebaseConnected | bool | Firebase 인증 완료 여부 | | firebaseError | String? | Firebase 연결 에러 메시지 | ## 주요 API ### firebaseAuthProvider MyIAM + Firebase 통합 인증 상태를 관리하는 Riverpod provider입니다. `firebaseAuthProvider 사용법` ```dart // 상태 감시 final authState = ref.watch(firebaseAuthProvider); // 토큰 갱신 (MyIAM + Firebase) await ref.read(firebaseAuthProvider.notifier).refreshToken(); // 로그아웃 (MyIAM + Firebase) await ref.read(firebaseAuthProvider.notifier).logout(); ``` ### firebaseAuthBridgeProvider Firebase Custom Token 브릿지 서비스를 제공하는 provider입니다. `firebaseAuthBridgeProvider 사용법` ```dart final bridge = ref.read(firebaseAuthBridgeProvider); // 현재 Firebase 사용자 final user = bridge.currentUser; // Firebase 인증 상태 스트림 bridge.authStateChanges.listen((user) { // ... }); ``` ## 커스터마이징 ### Cloud Function 이름 변경 기본 Cloud Function 이름은 `mintFirebaseToken`입니다. 다른 이름을 사용하려면 `firebaseAuthBridgeProvider`를 override하세요. `Cloud Function 이름 변경` ```dart ProviderScope( overrides: [ authConfigProvider.overrideWithValue(/* ... */), firebaseAuthBridgeProvider.overrideWithValue( FirebaseAuthBridge(functionName: 'myCustomFunction'), ), ], child: const MyApp(), ) ``` ### Firebase 인스턴스 커스터마이징 멀티 프로젝트 환경 등에서 별도의 Firebase 인스턴스를 사용할 수 있습니다. `Firebase 인스턴스 커스터마이징` ```dart firebaseAuthBridgeProvider.overrideWithValue( FirebaseAuthBridge( firebaseAuth: FirebaseAuth.instanceFor(app: secondaryApp), functions: FirebaseFunctions.instanceFor(app: secondaryApp), ), ) ``` ## Flutter Quickstart와의 차이점 | | Flutter SDK만 사용 | + Firebase SDK | | --- | --- | --- | | Provider | `authNotifierProvider` | `firebaseAuthProvider` | | 인증 상태 | `AuthState` | `FirebaseAuthState` | | 토큰 갱신 | `authNotifier.refreshToken()` | `firebaseAuthNotifier.refreshToken()` (Firebase 재연결 포함) | | 로그아웃 | `authNotifier.logout()` | `firebaseAuthNotifier.logout()` (Firebase signOut 포함) | | Firebase 접근 | 별도 구현 필요 | 자동 연결 | **프로젝트 구조** ``` my_app/ ├── lib/ │ ├── main.dart -- Firebase + MyIAM 초기화 │ ├── firebase_options.dart -- flutterfire configure로 생성 │ └── screens/ │ ├── home_screen.dart -- 로그인/회원가입 화면 │ └── dashboard_screen.dart -- Firebase 연결 상태 + 사용자 정보 ├── functions/ │ └── index.ts -- mintFirebaseToken Cloud Function ├── env.json -- 환경 변수 (gitignore 대상) └── pubspec.yaml -- 의존성 설정 ``` **다음 단계** - **Firestore 보안 규칙 설정** [쉬움] — request.auth.uid로 접근 제어를 설정하여 사용자별 데이터 보호하기 (uid는 MyIAM의 service_user_uid와 동일) - **Cloud Function 이름 커스터마이징** [쉬움] — 기본 mintFirebaseToken 대신 프로젝트에 맞는 Function 이름으로 변경하기 - **멀티 Firebase 프로젝트 연동** [어려움] — 별도의 Firebase 인스턴스를 사용하여 여러 Firebase 프로젝트를 동시에 연동하기 --- ## Supabase + Flutter 연동 가이드 URL: https://myiam.io/docs/guide/supabase-flutter MyIAM과 Supabase를 함께 사용하는 방법을 안내합니다. Edge Function을 통한 세션 브릿지와 계정 연결(UUID v5) 패턴. 샘플 코드: https://github.com/myiam-io/supabase-flutter-sample # Supabase + Flutter 연동 가이드 이 가이드는 Flutter, Dart, Supabase 환경 기준입니다. MyIAM을 인증 제공자로 사용하면서 Supabase 서비스(Database, Storage 등)를 함께 활용하는 방법을 안내합니다. MyIAM 로그인 후 자동으로 Supabase Auth에 연결되어 Supabase 서비스를 바로 사용할 수 있습니다. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - [Flutter Quickstart](https://myiam.io/docs/quickstart/flutter)를 완료하여 MyIAM 로그인이 동작하는 상태여야 합니다. > - [Supabase 프로젝트](https://supabase.com/dashboard) 생성이 완료되어야 합니다. > - `Supabase CLI`가 설치되어 있어야 합니다. (`brew install supabase/tap/supabase`) > - 서버 측에 `mint-supabase-token` Edge Function이 배포되어 있어야 합니다. (아래 2단계 참고) ## MyIAM 계정과 Supabase 계정의 연결 이 SDK의 핵심은 **MyIAM 사용자와 Supabase Auth 사용자를 1:1로 연결**하는 것입니다. MyIAM이 인증의 단일 소스(Single Source of Truth)이고, Supabase Auth는 Database RLS 등 Supabase 서비스 접근을 위한 브릿지 역할만 수행합니다. ### 왜 계정 연결이 필요한가? Supabase의 Database, Storage 등은 **Supabase Auth 사용자**를 기준으로 접근 제어(RLS)를 수행합니다. MyIAM으로 로그인한 사용자가 Supabase 서비스에 접근하려면, MyIAM 계정에 대응하는 Supabase Auth 사용자가 존재해야 하고, 유효한 Supabase 세션이 필요합니다. **MyIAM ↔ Supabase 계정 매핑** ```mermaid flowchart LR A["MyIAM 사용자
service_user_uid:
#quot;47DJDkD0Fz...#quot;
username: #quot;john#quot;
email: #quot;j@mail.com#quot;"] A -->|"1:1 매핑 (UUID v5)"| B["Supabase Auth 사용자
id (UUID):
#quot;a3f1b2c4-d5e6-5...#quot;
email: #quot;j@mail.com#quot;
user_metadata:
myiam_uid: #quot;47DJDk...#quot;
display_name: #quot;john#quot;"] ``` 계정 연결은 Edge Function(`mint-supabase-token`)에서 자동으로 수행됩니다. 앱 개발자가 별도로 구현할 내용은 없습니다. ### User ID 매핑: UUID v5 MyIAM의 `service_user_uid`는 base62 32자리 문자열이고, Supabase Auth의 user ID는 UUID만 허용합니다. 이 변환에 **UUID v5**(SHA-1 기반, RFC 4122)를 사용합니다. `UUID v5 변환 예시` ```typescript // Edge Function 내부 const MYIAM_NAMESPACE = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; const supabaseUserId = await uuidV5(myiamUid, MYIAM_NAMESPACE); ``` #### UUID v5 특성 | 속성명 | 설명 | | --- | --- | | 결정적 | 같은 MyIAM UID → 항상 같은 UUID. 매번 계산해도 동일한 결과 | | 대소문자 보존 | AbC ≠ abc → 다른 UUID 생성 (base62의 대소문자 구분 유지) | | 충돌 방지 | SHA-1 기반, 실질적 충돌 불가 | | 역변환 불가 | UUID에서 원본 MyIAM UID를 역추적할 수 없음 | **MYIAM_NAMESPACE는 모든 연동에서 동일해야 합니다.** 변경 시 기존 사용자의 UUID가 달라져 데이터 연결이 끊어집니다. ### Firebase 연동과의 차이 Firebase에서는 MyIAM `service_user_uid`를 Firebase Auth uid로 **그대로 사용**할 수 있지만, Supabase Auth는 user ID에 UUID 형식만 허용하므로 UUID v5 변환이 필요합니다. | | Firebase | Supabase | | --- | --- | --- | | MyIAM UID | 그대로 uid로 사용 | UUID v5로 변환 후 id로 사용 | | 원본 UID 보존 | uid에 원본 그대로 | `user_metadata.myiam_uid`에 저장 | | 조회 방식 | `getUser(uid)` | `getUserById(uuidV5(uid))` | ### 프로필 동기화 매 로그인 시 MyIAM의 최신 프로필 정보가 Supabase Auth의 `user_metadata`와 `email` 필드에 동기화됩니다. `프로필 동기화 (Edge Function 내부)` ```typescript // Edge Function 내부 await admin.auth.admin.updateUserById(supabaseUserId, { email, // 실제 email (또는 {uuid}@myiam.internal fallback) user_metadata: { myiam_uid: "47DJDk...", // 원본 MyIAM UID (역참조용) display_name: "john", // profile.name 또는 username myiam_email: "j@...", // MyIAM 프로필 email nickname: "...", gender: "...", // ... 기타 프로필 필드 }, email_confirm: true, }); ``` #### Supabase Auth 필드 매핑 | 속성명 | 타입 | 설명 | | --- | --- | --- | | id (UUID) | UUID v5(myiam_uid) | RLS auth.uid(), 테이블 외래키 | | email | MyIAM 프로필 email | Dashboard 사용자 식별, generateLink | | user_metadata.myiam_uid | 원본 MyIAM UID | MyIAM 시스템과 역참조 | | user_metadata.display_name | profile.name / username | 앱에서 표시명 | | user_metadata.* | MyIAM 프로필 각 필드 | 앱에서 추가 정보 활용 | ### Step 1: SDK 설치 `pubspec.yaml`의 `dependencies`에 아래 패키지를 추가합니다. `pubspec.yaml` ```yaml dependencies: flutter: sdk: flutter flutter_riverpod: ^3.3.1 myiam_flutter_sdk: ^0.8.1 myiam_supabase_flutter_sdk: ^0.3.6 supabase_flutter: ^2.12.4 ``` - `myiam_supabase_flutter_sdk` — MyIAM + Supabase 통합 인증 SDK - `supabase_flutter` — Supabase Flutter 클라이언트 `터미널` ```bash flutter pub get ``` ### Step 2: Edge Function 배포 #### 왜 Edge Function이 필요한가요? MyIAM과 Supabase는 각각 독립된 Identity Provider(IdP)입니다. MyIAM OAuth2 로그인으로 발급받은 access_token은 MyIAM API에서만 유효하며, Supabase RLS는 **Supabase Auth가 발급한 세션**만 인식합니다. 따라서 MyIAM access_token만으로는 Database, Storage 등 Supabase 서비스에 접근할 수 없습니다. 이 문제를 해결하기 위해 Edge Function이 MyIAM access_token을 검증한 후, Supabase Auth 세션을 발급합니다. 이 과정에서 [계정 연결](#account-linking)(UUID v5 변환, 사용자 조회/생성, 프로필 동기화)도 자동으로 수행됩니다. **세션 브릿지 흐름** ```mermaid sequenceDiagram participant app as Flutter 앱 participant fn as Edge Function participant server as MyIAM 서버 app->>fn: access_token 전달 (mint-supabase-token 호출) fn->>server: 토큰 검증 요청 (@myiam.io/web-sdk로 검증) server-->>fn: service_user_uid 반환 (토큰 유효 시 사용자 식별자 응답) Note over fn: 계정 연결
UUID v5 변환 → 사용자 조회/생성 → 프로필 동기화 Note over fn: 세션 발급
generateLink + verifyOtp fn-->>app: 세션 반환 (access_token + refresh_token) Note over app: Supabase 로그인
setSession(refreshToken) ``` #### Edge Function 구현 Edge Function은 다음을 수행합니다: 1. API key 검증 (publishable key) 2. MyIAM access_token 검증 (`@myiam.io/web-sdk`) 3. MyIAM UID → UUID v5 변환 (Supabase Auth user ID로 사용) 4. Supabase Auth 사용자 조회/생성 + 프로필 동기화 5. generateLink + verifyOtp로 세션 발급 #### 설정 `config.toml`에 JWT 검증 비활성화를 추가합니다 (publishable key 사용 시 필요): `config.toml` ```toml [functions.mint-supabase-token] verify_jwt = false ``` Edge Function 내부에서 `apikey` 헤더를 직접 검증하므로 보안에 문제 없습니다. #### 환경변수 및 배포 `환경변수 설정` ```bash supabase secrets set MYIAM_SERVICE_UID=<값> MYIAM_API_KEY=<값> ``` `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, `SUPABASE_ANON_KEY`는 Edge Function에 자동 주입됩니다. `배포` ```bash supabase functions deploy mint-supabase-token ``` 구현 예시는 [supabase-flutter-sample](https://github.com/myiam-io/supabase-flutter-sample)을 참고하세요. Edge Function 이름은 기본적으로 `mint-supabase-token`입니다. 다른 이름을 사용하려면 [커스터마이징](#code-custom-function) 섹션을 참고하세요. ### Step 3: Supabase 초기화 `main.dart`에서 기존 MyIAM 초기화 코드에 Supabase 초기화를 추가합니다. `Supabase.initialize()`를 `runApp()` 전에 호출합니다. `lib/main.dart` ```dart // lib/main.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; import 'package:myiam_supabase_flutter_sdk/myiam_supabase_flutter_sdk.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: const String.fromEnvironment('SUPABASE_URL'), anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'), ); runApp( ProviderScope( overrides: [ authConfigProvider.overrideWithValue( const MyiamConfig( serviceUid: String.fromEnvironment('SERVICE_UID'), oauth2ClientId: String.fromEnvironment('OAUTH2_CLIENT_ID'), apiKey: String.fromEnvironment('API_KEY'), redirectScheme: String.fromEnvironment('REDIRECT_SCHEME'), redirectHost: String.fromEnvironment('REDIRECT_HOST'), ), ), ], child: const MyApp(), ), ); } ``` ### Step 4: 인증 상태를 Supabase 통합 상태로 전환 기존 `authNotifierProvider` 대신 `supabaseAuthProvider`를 사용하여 MyIAM + Supabase 통합 인증 상태를 관리합니다. `lib/main.dart (계속)` ```dart class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(supabaseAuthProvider); return MaterialApp( title: 'MyIAM Supabase Flutter', theme: ThemeData( colorSchemeSeed: Colors.deepOrange, useMaterial3: true, ), home: switch (authState) { SupabaseAuthLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), SupabaseAuthAuthenticated() => const DashboardScreen(), SupabaseAuthUnauthenticated() => const HomeScreen(), SupabaseAuthError() => const HomeScreen(), }, ); } } ``` `supabaseAuthProvider`는 내부적으로 `authNotifierProvider`를 감시합니다. MyIAM 로그인이 완료되면 자동으로 Supabase 세션 브릿지를 연결합니다. ### Step 5: 대시보드에서 Supabase 연결 상태 확인 `SupabaseAuthAuthenticated` 상태에는 Supabase 연결 정보가 포함됩니다. 연결 상태, 에러 메시지, 토큰 갱신 기능을 대시보드에서 확인할 수 있습니다. `lib/screens/dashboard_screen.dart` ```dart // lib/screens/dashboard_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:myiam_supabase_flutter_sdk/myiam_supabase_flutter_sdk.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(supabaseAuthProvider); return authState is! SupabaseAuthAuthenticated ? const Scaffold(body: Center(child: CircularProgressIndicator())) : Scaffold( appBar: AppBar( title: const Text('대시보드'), actions: [ TextButton.icon( onPressed: () async { await ref.read(supabaseAuthProvider.notifier).logout(); }, icon: const Icon(Icons.logout), label: const Text('로그아웃'), ), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ Text('사용자: ${authState.myiamUser.username}'), Text('UID: ${authState.myiamUser.uid}'), const Divider(), // Supabase 연결 상태 if (authState.supabaseConnected) const Text('Supabase: 연결됨', style: TextStyle(color: Colors.green)) else if (authState.supabaseError != null) Text('Supabase 오류: ${authState.supabaseError}', style: const TextStyle(color: Colors.red)) else const Text('Supabase: 연결 중...'), const SizedBox(height: 16), FilledButton.icon( onPressed: () async { final messenger = ScaffoldMessenger.of(context); try { await ref .read(supabaseAuthProvider.notifier) .refreshToken(); messenger.showSnackBar( const SnackBar(content: Text('토큰이 갱신되었습니다')), ); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('토큰 갱신 실패: $e')), ); } }, icon: const Icon(Icons.refresh), label: const Text('토큰 갱신'), ), ], ), ); } } ``` ### Step 6: Database 사용 예시 Supabase 인증이 완료되면 Database에 바로 접근할 수 있습니다. SDK가 제공하는 `supabaseClientProvider`를 사용합니다. `Database 접근 예시` ```dart import 'package:myiam_supabase_flutter_sdk/myiam_supabase_flutter_sdk.dart'; // Supabase 인증이 완료된 상태에서 final client = ref.read(supabaseClientProvider); final userId = client.auth.currentUser!.id; // 데이터 조회 final notes = await client.from('notes').select(); // 데이터 삽입 await client.from('notes').insert({'text': 'Hello', 'user_id': userId}); ``` RLS 정책에서 `auth.uid()`는 UUID v5 변환된 값을 반환합니다. 테이블과 정책 설정 예시: `RLS 정책 설정 예시` ```sql -- 테이블 생성 CREATE TABLE public.notes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, text text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); -- RLS 정책: 본인 데이터만 접근 ALTER TABLE public.notes ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can manage own notes" ON public.notes FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ``` ### Step 7: 딥링크 설정 **Android** — `AndroidManifest.xml`의 `` 안에 intent-filter를 추가합니다. `AndroidManifest.xml` ```xml ``` **iOS** — `ios/Runner/Info.plist`의 `` 안에 URL Scheme을 추가합니다. `Info.plist`는 scheme 단위로만 등록되므로 host는 지정하지 않습니다. `Info.plist` ```xml CFBundleURLTypes CFBundleURLSchemes myiamsupabase ``` `scheme`과 `host`는 `env.json`의 `REDIRECT_SCHEME`, `REDIRECT_HOST`와 일치해야 하며, MyIAM 콘솔의 OAuth2 클라이언트 redirect URI에도 등록해야 합니다. ### Step 8: 환경변수 설정 `env.json` ```json { "SERVICE_UID": "", "OAUTH2_CLIENT_ID": "", "API_KEY": "", "REDIRECT_SCHEME": "myiamsupabase", "REDIRECT_HOST": "oauth2callback", "SUPABASE_URL": "https://xxxxx.supabase.co", "SUPABASE_ANON_KEY": "sb_publishable_..." } ``` `SUPABASE_ANON_KEY`는 publishable key(`sb_publishable_...`) 형식을 사용합니다. ### Step 9: 실행 모든 파일을 저장하고 아래 명령어로 앱을 실행합니다. `터미널` ```bash flutter pub get flutter run --dart-define-from-file=env.json ``` 로그인 후 대시보드에서 Supabase 연결 상태가 "연결됨"으로 표시되는지 확인하세요. **전체 인증 흐름** ```mermaid flowchart TD A["MyIAM 로그인 완료
LoginScreen / SignupScreen에서 인증 완료"] A --> B["Supabase 브릿지 자동 연결
supabaseAuthProvider가 MyIAM 인증 상태를 감지합니다"] B --> C["Edge Function 호출
mint-supabase-token에 MyIAM access_token을 전송합니다"] C --> D["계정 연결 및 세션 발급
MyIAM UID → UUID v5 변환, 사용자 조회/생성, 프로필 동기화, 세션 발급"] D --> E["Supabase 인증 완료
setSession(refreshToken)으로 Supabase Auth 로그인"] E --> F["Supabase 서비스 사용 가능
Database, Storage 등 Supabase 서비스에 바로 접근 가능"] G["로그아웃
Supabase signOut() + MyIAM logout() 양쪽 모두 세션 정리"] ``` ## 인증 상태 (SupabaseAuthState) #### 상태 | 속성명 | 설명 | | --- | --- | | SupabaseAuthLoading | 초기 로딩 중 | | SupabaseAuthAuthenticated | MyIAM 인증 완료 (Supabase 연결 진행 중 또는 완료) | | SupabaseAuthUnauthenticated | 미인증 | | SupabaseAuthError | 에러 발생 | #### SupabaseAuthAuthenticated 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | myiamTokens | MyiamTokens | MyIAM access/refresh 토큰 | | myiamUser | MyiamUser | MyIAM 사용자 정보 | | supabaseConnected | bool | Supabase 인증 완료 여부 | | supabaseError | String? | Supabase 연결 에러 메시지 | ## 주요 API ### supabaseAuthProvider MyIAM + Supabase 통합 인증 상태를 관리하는 Riverpod provider입니다. `supabaseAuthProvider 사용법` ```dart // 상태 감시 final authState = ref.watch(supabaseAuthProvider); // 토큰 갱신 (MyIAM + Supabase) await ref.read(supabaseAuthProvider.notifier).refreshToken(); // 로그아웃 (MyIAM + Supabase) await ref.read(supabaseAuthProvider.notifier).logout(); ``` ### supabaseClientProvider Supabase 클라이언트 인스턴스를 제공하는 provider입니다. `supabaseClientProvider 사용법` ```dart final client = ref.read(supabaseClientProvider); // Database final data = await client.from('notes').select(); // Storage final file = await client.storage.from('avatars').download('avatar.png'); ``` ### supabaseAuthBridgeProvider Supabase 세션 브릿지 서비스를 제공하는 provider입니다. `supabaseAuthBridgeProvider 사용법` ```dart final bridge = ref.read(supabaseAuthBridgeProvider); // 현재 Supabase 사용자 final user = bridge.currentUser; // Supabase 인증 상태 스트림 bridge.authStateChanges.listen((state) { // ... }); ``` ## 커스터마이징 ### Edge Function 이름 변경 기본 Edge Function 이름은 `mint-supabase-token`입니다. 다른 이름을 사용하려면 `supabaseAuthBridgeProvider`를 override하세요. `Edge Function 이름 변경` ```dart ProviderScope( overrides: [ authConfigProvider.overrideWithValue(/* ... */), supabaseAuthBridgeProvider.overrideWithValue( SupabaseAuthBridge(functionName: 'myCustomFunction'), ), ], child: const MyApp(), ) ``` ### Supabase 클라이언트 커스터마이징 별도의 Supabase 인스턴스를 사용할 수 있습니다. `Supabase 클라이언트 커스터마이징` ```dart supabaseAuthBridgeProvider.overrideWithValue( SupabaseAuthBridge( supabase: myCustomSupabaseClient, ), ) ``` ## Flutter Quickstart와의 차이점 | | Flutter SDK만 사용 | + Supabase SDK | | --- | --- | --- | | Provider | `authNotifierProvider` | `supabaseAuthProvider` | | 인증 상태 | `AuthState` | `SupabaseAuthState` | | 토큰 갱신 | `authNotifier.refreshToken()` | `supabaseAuthNotifier.refreshToken()` (Supabase 재연결 포함) | | 로그아웃 | `authNotifier.logout()` | `supabaseAuthNotifier.logout()` (Supabase signOut 포함) | | Supabase 접근 | 별도 구현 필요 | 자동 연결 | ## Firebase 연동과의 차이점 | | Firebase | Supabase | | --- | --- | --- | | 백엔드 함수 | Cloud Function | Edge Function (Deno) | | 토큰 교환 | Custom Token → `signInWithCustomToken()` | generateLink + verifyOtp → `setSession()` | | User ID | MyIAM UID 그대로 사용 | MyIAM UID → UUID v5 변환 | | 프로필 저장 | Auth 필드 + Custom Claims | `user_metadata` | | 접근 제어 | Firestore Security Rules | Row Level Security (RLS) | | API Key | JWT 기반 | Publishable key (`verify_jwt = false`) | **프로젝트 구조** ``` my_app/ ├── lib/ │ ├── main.dart -- Supabase + MyIAM 초기화 │ └── screens/ │ ├── home_screen.dart -- 로그인/회원가입 화면 │ └── dashboard_screen.dart -- Supabase 연결 상태 + 사용자 정보 ├── supabase/ │ ├── functions/ │ │ └── mint-supabase-token/ │ │ └── index.ts -- Edge Function (토큰 교환 + 계정 연결) │ └── config.toml -- JWT 검증 비활성화 설정 ├── android/app/src/main/AndroidManifest.xml -- 딥링크 설정 ├── ios/Runner/Info.plist -- URL Scheme 설정 ├── env.json -- 환경 변수 (gitignore 대상) └── pubspec.yaml -- 의존성 설정 ``` **다음 단계** - **RLS 정책 설정** [쉬움] — auth.uid()로 접근 제어를 설정하여 사용자별 데이터를 보호하세요 (uid는 UUID v5 변환된 값) - **Edge Function 이름 커스터마이징** [쉬움] — 기본 mint-supabase-token 대신 프로젝트에 맞는 Function 이름으로 변경하기 - **Storage 연동** [보통] — Supabase Storage에 RLS 정책을 적용하여 사용자별 파일 업로드/다운로드 구현하기 - **별도 Supabase 인스턴스 연동** [어려움] — 별도의 Supabase 클라이언트를 사용하여 여러 프로젝트를 동시에 연동하기 --- ## HTTP API 레퍼런스 URL: https://myiam.io/docs/api MyIAM HTTP API 문서 - 회원, 토큰, 사용자, 서비스 사용자 엔드포인트를 확인하세요. # HTTP API 레퍼런스 MyIAM HTTP API 엔드포인트를 확인하세요. ## 회원 API 수동 회원 가입 및 탈퇴를 처리하는 API입니다. 서비스에서 회원 가입 승인 작업을 직접 수행하려는 경우에 사용합니다. #### `POST` `/api/v0/account/prepare/complete` — 회원 가입 준비 완료 https://myiam.io/docs/api#account-prepare-complete 회원 가입 준비를 완료하고 다음 단계 리다이렉트 URL을 반환하는 API **Headers** | Header | Value | | --- | --- | | `My-Key` | KEY `` | | `My-Service` | UID `` | - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. #### Request Body Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | String | 가입 준비 콜백 URL로 전달된 토큰 | | profile_data | Object | 사용자 프로필 데이터 (name, email, nickname, custom_fields 등) | #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | redirect_url * | String | 다음 단계 리다이렉트 URL | #### Response ```json { "redirect_url": "https://example.com/welcome" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 400 | 400 | 잘못된 요청 또는 토큰 만료 | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | #### `POST` `/api/v0/account/register/complete` — 수동 회원 가입 완료 https://myiam.io/docs/api#account-register-complete 수동으로 사용자 가입을 완료하기 위한 API. 서비스 수동 가입이 활성화된 경우, 수동 회원 가입 콜백 URL로 전달된 토큰을 사용하여 가입을 확정합니다. **서비스 수동 가입이란?** 회원 가입 승인 작업을 서비스에서 직접 수행하려 할 경우, 서비스 수동 가입을 활성화시켜 서비스 주소로 이동해서 완료 API를 호출해야 가입이 완료됩니다. 완료 API를 호출하면 가입된 회원 정보를 모두 획득할 수 있습니다. 승인 목적 외에 서비스에서 회원 정보를 획득하기 위한 목적으로도 사용할 수 있습니다. **Headers** | Header | Value | | --- | --- | | `My-Key` | KEY `` | | `My-Service` | UID `` | - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. #### Request Body Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | String | 수동 회원 가입 콜백 URL로 전달된 서비스 가입 토큰 | | custom_fields | Object | 사용자 정의 필드 | #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | redirect_url * | String | 가입 완료 후 리다이렉트 URL | | user | Object | 사용자 정보 | | user.uid * | String | 서비스 사용자 UID | | user.status * | String | 사용자 상태 (NORMAL, UNUSED, BLOCKED, DEREGISTERED) | | user.created_at * | LocalDateTime | 가입 일시 | | user.profiles | Object | 사용자 프로필 (email, name, nickname, gender, birth_year, birthday, date_of_birth, mobile_number, home_number, phone_number, address, custom_fields) | #### Response ```json { "redirect_url": "https://example.com/welcome", "user": { "uid": "xxxxxxxxxxxxxxxx", "status": "NORMAL", "created_at": "2026-03-13T12:00:00", "profiles": { "email": "user@example.com", "name": "홍길동", "nickname": "길동", "gender": "M", "birth_year": "1990", "birthday": "0101", "date_of_birth": "19900101", "mobile_number": "01012345678", "home_number": null, "phone_number": null, "address": { "address1": "서울특별시 강남구 테헤란로 123", "address2": "456호", "zipcode": "06234" }, "custom_fields": { "company": "MyIAM", "role": "admin" } } } } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 404 | 404 | 토큰이 존재하지 않거나 만료된 경우 | | 400 | 400 | 잘못된 요청 | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | #### `POST` `/api/v0/account/deregister/complete` — 수동 회원 탈퇴 완료 https://myiam.io/docs/api#account-deregister-complete 수동으로 사용자 탈퇴를 완료하기 위한 API **Headers** | Header | Value | | --- | --- | | `My-Key` | KEY `` | | `My-Service` | UID `` | - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. #### Request Body Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | String | prepare API에서 발급받은 탈퇴 토큰 | | type | String | 기본 탈퇴 화면을 사용하지 않는 경우 탈퇴 이유 타입 값 | | reason | String | 기본 탈퇴 화면을 사용하지 않는 경우 탈퇴 이유 문자열 | | message | String | 기본 탈퇴 화면을 사용하지 않는 경우 탈퇴 메시지 | #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | redirect_url * | String | 탈퇴 완료 후 리다이렉트 URL | | service_user_uid | String | 탈퇴 처리된 서비스 사용자 UID | #### Response ```json { "redirect_url": "https://example.com/goodbye", "service_user_uid": "xxxxxxxxxxxxxxxx" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 400 | 400 | 잘못된 요청 또는 토큰 만료 | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | ## 토큰 API Access Token의 유효성 조회 및 삭제를 처리하는 API입니다. #### `GET` `/api/v0/token/info` — 토큰 정보 조회 https://myiam.io/docs/api#token-info 현재 인증된 Access Token의 유효성 및 정보를 조회하는 API **Headers** | Header | Value | | --- | --- | | `Authorization` | Bearer `` | | `My-Key` | KEY `` | | `My-Service` | UID `` | - ``: SDK의 myiamHandleCallback() 또는 myiamRefreshToken() 응답에서 access_token 값을 사용하세요. - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | active * | Boolean | 토큰 활성 여부 | | expired_at | LocalDateTime | 토큰 만료 시각 | | service_user_uid | String | 서비스 사용자 UID | #### Response ```json { "active": true, "expired_at": "2026-03-13T12:00:00", "service_user_uid": "xxxxxxxxxxxxxxxx" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 404 | 404 | Authorization 헤더가 없는 경우 | | 401 | 401 | 인증 실패 (토큰 없음 또는 만료) | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | #### `POST` `/api/v0/token/delete` — 토큰 삭제 https://myiam.io/docs/api#token-delete Access Token을 만료 처리하는 API. target 파라미터로 삭제 범위를 지정할 수 있습니다. #### Parameters | 속성명 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | target | String | (없음) | 삭제 범위 | | refresh_token_delete | Boolean | true | Refresh Token도 함께 삭제할지 여부 | #### target 값 설명 | 속성명 | 설명 | | --- | --- | | (미지정) | 현재 Access Token만 삭제 | | ACCOUNT_USER | 해당 계정 사용자의 모든 토큰 삭제 | | SERVICE | 서비스 전체 사용자의 모든 토큰 삭제 | **Headers** | Header | Value | | --- | --- | | `Authorization` | Bearer `` | | `My-Key` | KEY `` | | `My-Service` | UID `` | | `My-Service-User` | UID `` | - ``: SDK의 myiamHandleCallback() 또는 myiamRefreshToken() 응답에서 access_token 값을 사용하세요. - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. - ``: 사용자 정보 조회 API(/api/v0/user/me)의 응답에서 uid 값을 사용하세요. #### Response ```json { "_code": 200, "_message": "OK" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 404 | 404 | Authorization 헤더가 없는 경우 | | 401 | 401 | 인증 실패 (토큰 없음 또는 만료) | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | ## 사용자 API 현재 인증된 사용자의 정보 조회 및 동작 준비를 처리하는 API입니다. #### `GET` `/api/v0/user/me` — 사용자 정보 조회 https://myiam.io/docs/api#user-me 현재 인증된 사용자의 정보를 조회하는 API **Headers** | Header | Value | | --- | --- | | `Authorization` | Bearer `` | | `My-Key` | KEY `` | | `My-Service` | UID `` | | `My-Service-User` | UID `` | - ``: SDK의 myiamHandleCallback() 또는 myiamRefreshToken() 응답에서 access_token 값을 사용하세요. - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. - ``: 사용자 정보 조회 API(/api/v0/user/me)의 응답에서 uid 값을 사용하세요. #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | uid | String | 서비스 사용자 UID | | service_uid | String | 서비스 UID | | username | String | 사용자 아이디 (IdP username) | | authority | Array\ | 부여된 권한 목록 | #### Response ```json { "uid": "xxxxxxxxxxxxxxxx", "service_uid": "xxxxxxxxxxxxxxxx", "username": "user@example.com", "authority": [] } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 401 | 401 | 인증 실패 (토큰 없음 또는 만료) | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | #### `POST` `/api/v0/user/prepare/{type}` — 사용자 동작 준비 https://myiam.io/docs/api#user-prepare-type 특정 사용자 동작을 위한 임시 토큰을 발급하고, 해당 동작 화면으로 이동할 수 있는 리다이렉트 URL을 반환하는 API 반환된 `redirect_url`에 포함된 토큰은 **15분** 동안 유효합니다. #### 동작 타입 (type) | 속성명 | 설명 | | --- | --- | | deregister | 회원 탈퇴 | | set-password | 비밀번호 설정 | | reset-password | 비밀번호 재설정 | | edit-profile | 프로필 수정 | | edit-email | 이메일 수정 | | passkey | 패스키(Passkey) 관리 | **Headers** | Header | Value | | --- | --- | | `Authorization` | Bearer `` | | `My-Key` | KEY `` | | `My-Service` | UID `` | | `My-Service-User` | UID `` | - ``: SDK의 myiamHandleCallback() 또는 myiamRefreshToken() 응답에서 access_token 값을 사용하세요. - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. - ``: 사용자 정보 조회 API(/api/v0/user/me)의 응답에서 uid 값을 사용하세요. #### Request Body Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | complete_redirect_url * | String | 동작 완료 후 리다이렉트할 URL | | state | String | 완료 후 전달받을 임의 상태값 (complete_redirect_url에 쿼리로 추가됨) | | edit_params.fields | Array\ | edit-profile 타입에서 수정 가능한 필드 목록 | | lang | String | 동작 화면을 표시할 언어 (`ko`, `en`, `ja`, `zh`). 지정하면 `redirect_url`에 `lang` 파라미터로 포함되며, 이후 같은 세션의 화면도 해당 언어로 유지됩니다. 생략하거나 지원하지 않는 값이면 브라우저의 `Accept-Language`를 따릅니다 | #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | redirect_url * | String | 동작 화면으로 이동할 URL (임시 토큰 포함, 15분 유효) | #### Response ```json { "redirect_url": "https://auth.example.com/s/{서비스UID}/set-password?token=xxxxxxxx&lang=en" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 400 | 400 | 서비스에서 해당 type이 허용되지 않는 경우 / 잘못된 요청 | | 401 | 401 | 인증 실패 (토큰 없음 또는 만료) | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | ## 서비스 사용자 API 현재 인증된 서비스 사용자의 기본 정보 및 프로필을 조회하는 API입니다. #### `GET` `/api/v0/service-user/get` — 서비스 사용자 조회 https://myiam.io/docs/api#service-user-get 현재 인증된 서비스 사용자의 기본 정보를 조회하는 API **Headers** | Header | Value | | --- | --- | | `Authorization` | Bearer `` | | `My-Key` | KEY `` | | `My-Service` | UID `` | | `My-Service-User` | UID `` | - ``: SDK의 myiamHandleCallback() 또는 myiamRefreshToken() 응답에서 access_token 값을 사용하세요. - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. - ``: 사용자 정보 조회 API(/api/v0/user/me)의 응답에서 uid 값을 사용하세요. #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | id | Long | 서비스 사용자 ID | | uid * | String | 서비스 사용자 UID | | service_id | Long | 서비스 ID | | status * | String | 사용자 상태 (NORMAL, UNUSED, BLOCKED, DEREGISTERED) | | visited_at | LocalDateTime | 최종 방문 일시 | | created_at * | LocalDateTime | 가입 일시 | | updated_at | LocalDateTime | 최종 수정 일시 | #### Response ```json { "id": 1, "uid": "xxxxxxxxxxxxxxxx", "service_id": 1, "status": "NORMAL", "visited_at": "2026-03-13T12:00:00", "created_at": "2026-03-13T12:00:00", "updated_at": "2026-03-13T12:00:00" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 401 | 401 | 인증 실패 (토큰 없음 또는 만료) | | 404 | 404 | 사용자를 찾을 수 없는 경우 | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | #### `GET` `/api/v0/service-user/profile/get` — 서비스 사용자 프로필 조회 https://myiam.io/docs/api#service-user-profile-get 현재 인증된 서비스 사용자의 프로필 정보를 조회하는 API **Headers** | Header | Value | | --- | --- | | `Authorization` | Bearer `` | | `My-Key` | KEY `` | | `My-Service` | UID `` | | `My-Service-User` | UID `` | - ``: SDK의 myiamHandleCallback() 또는 myiamRefreshToken() 응답에서 access_token 값을 사용하세요. - ``: 서비스 설정에서 API 키 생성 시 한 번만 표시되며, 이후에는 다시 확인할 수 없습니다. 생성 시 안전한 곳에 보관하고 해당 값을 사용하세요. - ``: 사용자 정보 조회 API(/api/v0/user/me)의 응답에서 uid 값을 사용하세요. #### Response Fields | 속성명 | 타입 | 설명 | | --- | --- | --- | | service_user_uid * | String | 서비스 사용자 UID | | service_user_id | Long | 서비스 사용자 ID | | status * | String | 사용자 프로필 상태 | | data | Object | 프로필 데이터 (email, name, nickname, gender, birth_year, birthday, date_of_birth, mobile_number, home_number, phone_number, address) | | created_at * | LocalDateTime | 가입 일시 | | updated_at | LocalDateTime | 최종 수정 일시 | #### Response ```json { "service_user_uid": "xxxxxxxxxxxxxxxx", "service_user_id": 1, "status": "NORMAL", "data": { "email": "user@example.com", "name": "홍길동", "nickname": "길동", "gender": "M", "birth_year": "1990", "birthday": "0101", "date_of_birth": "19900101", "mobile_number": "01012345678", "home_number": null, "phone_number": null, "address": { "address1": "서울특별시 강남구 테헤란로 123", "address2": "456호", "zipcode": "06234" } }, "created_at": "2026-03-13T12:00:00", "updated_at": "2026-03-13T12:00:00" } ``` #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 401 | 401 | 인증 실패 (토큰 없음 또는 만료) | | 404 | 404 | 사용자를 찾을 수 없는 경우 | | 500 | 999 | 서버 내부의 알 수 없는 원인으로 발생한 에러 | --- ## MyIAM CLI 설치 URL: https://myiam.io/docs/cli/install 터미널과 AI 코딩 에이전트로 MyIAM 서비스 설정을 관리하는 myiam-cli 바이너리 설치와 Claude Code/Codex CLI/Antigravity CLI용 AI 에이전트 플러그인 설치 방법을 안내합니다. 플러그인 코드: https://github.com/myiam-io/myiam-cli-plugin # MyIAM CLI 설치 이 가이드는 Claude Code, Codex CLI, Antigravity CLI 환경 기준입니다. 터미널에서 MyIAM 서비스 설정을 관리하는 CLI입니다. Claude Code, Codex CLI, Antigravity CLI 같은 AI 코딩 에이전트가 주 사용자로 설계되어 있어, CLI 바이너리를 설치하고 로그인한 뒤 AI 에이전트용 플러그인을 설치하면 자연어 요청만으로 서비스 설정을 바꿀 수 있습니다. **CLI에서 지원하지 않는 작업은 웹 콘솔 전용입니다** 보안·감사 로그가 필요한 작업이라 CLI에 의도적으로 넣지 않았습니다. 예를 들면: - Client Secret / API Key 생성 - 조직(테넌트)·서비스 생성 및 파기 - 운영자 권한 위임 - 최종 가입자(회원) 목록 조회 및 관리 ### Step 1: 사전 조건: CLI 바이너리 설치와 로그인 MyIAM CLI는 이미 만들어진 서비스의 설정만 관리합니다. myiam.io 가입과 서비스 생성은 먼저 웹 콘솔에서 진행하세요. > **사전 준비** > > MyIAM 계정이 필요합니다. 로그인 또는 회원가입 후 진행하세요. > > - macOS 또는 Linux에서 `Homebrew`가 설치되어 있어야 합니다. `macOS / Linux` ```bash brew tap myiam-io/tap brew install myiam-cli myiam-cli login ``` `Windows` ```bash winget install myiam.Cli myiam-cli login ``` `myiam-cli login`을 실행하면 브라우저가 열리고, 로그인 후 관리할 서비스를 선택하라는 안내가 표시됩니다. 기본 브라우저가 아닌 브라우저로 열려면 `MYIAM_BROWSER` 환경변수를 설정하세요. `chrome`, `safari`, `firefox`, `edge`, `brave`, `arc`, `terminal-browser`를 쓸 수 있고, `none`이면 브라우저를 열지 않고 터미널에 출력된 주소를 직접 엽니다. 실행 명령을 직접 지정하려면 `custom`과 `MYIAM_BROWSER_COMMAND`를 함께 설정합니다(`%s` 자리에 주소가 들어갑니다). `브라우저 지정` ```bash export MYIAM_BROWSER=chrome export MYIAM_BROWSER=custom MYIAM_BROWSER_COMMAND='firefox --private-window %s' ``` 로그아웃도 같은 브라우저로 열어야 그 브라우저의 로그인 상태가 함께 정리됩니다. ### Step 2: Claude Code 플러그인 설치 마켓플레이스를 등록하고 플러그인을 설치하면, Claude Code가 `/myiam` 스킬로 CLI 명령어 전체를 사용할 수 있습니다. `터미널` ```bash claude plugin marketplace add myiam-io/myiam-cli-plugin claude plugin install myiam@myiam ``` `업데이트 / 제거` ```bash claude plugin update myiam # 업데이트 claude plugin uninstall myiam # 제거 ``` ### Step 3: OpenAI Codex CLI 플러그인 설치 Codex CLI도 동일하게 마켓플레이스 등록 후 플러그인을 설치합니다. `터미널` ```bash codex plugin marketplace add myiam-io/myiam-cli-plugin codex plugin install myiam ``` `업데이트 / 제거` ```bash codex plugin upgrade myiam # 업데이트 codex plugin uninstall myiam # 제거 ``` ### Step 4: Google Antigravity CLI 플러그인 설치 Antigravity CLI는 별도 마켓플레이스 등록 없이 저장소 URL로 바로 플러그인을 설치합니다. `터미널` ```bash agy plugin install https://github.com/myiam-io/myiam-cli-plugin ``` `업데이트 / 제거` ```bash agy plugin uninstall myiam # 제거 (업데이트는 설치 명령을 다시 실행) ``` 전환기 동안에는 기존 Gemini CLI(`gemini extensions install https://github.com/myiam-io/myiam-cli-plugin`)로도 설치됩니다. **다음 단계** - **AI 에이전트에게 요청해보기** [쉬움] — "로그인 방법 목록 보여줘" 같은 간단한 요청부터 시작하기 - **서비스 설정 살펴보기** [쉬움] — 관리자 콘솔에서 현재 로그인 방법, 약관, 정책 설정을 확인해보기 --- ## Web SDK API 레퍼런스 URL: https://myiam.io/docs/sdk/web @myiam.io/web-sdk — 브라우저 및 서버 환경을 위한 OAuth2/PKCE 인증, 세션 관리, HTTP API SDK 문서. 패키지: https://www.npmjs.com/package/@myiam.io/web-sdk 릴리스 노트: https://myiam.io/docs/release-notes/web-sdk # Web SDK 브라우저 및 서버 환경을 위한 OAuth2/PKCE 인증, 세션 관리, HTTP API SDK. [Client SDK브라우저 환경을 위한 OAuth2/PKCE 인증 SDK. 로그인·회원가입·로그아웃·토큰 갱신을 지원합니다.](https://myiam.io/docs/sdk/web/client) [Server SDK서버 환경(Node.js, Edge Runtime)을 위한 쿠키 기반 세션 관리 및 HTTP API SDK. REST API만 필요하면 @myiam.io/web-sdk/api 진입점을 사용합니다.](https://myiam.io/docs/sdk/web/server) --- ## Web SDK Client API 레퍼런스 URL: https://myiam.io/docs/sdk/web/client @myiam.io/web-sdk/client - 브라우저 환경을 위한 OAuth2/PKCE 인증 SDK 문서. 패키지: https://www.npmjs.com/package/@myiam.io/web-sdk 릴리스 노트: https://myiam.io/docs/release-notes/web-sdk # Client SDK API 레퍼런스 브라우저 환경을 위한 OAuth2/PKCE 인증 SDK. 외부 의존성 없이 동작하며, 로그인·회원가입·로그아웃·토큰 갱신을 지원합니다. ## 시작 가이드 설치, 빠른 시작, 인증 플로우 ### 설치 ```bash npm install @myiam.io/web-sdk ``` ### 빠른 시작 ```typescript import { myiamInit, myiamLoginUrl, myiamHandleCallback, myiamRefreshToken, myiamLogoutUrl, } from '@myiam.io/web-sdk/client' // 1. 초기화 (앱 시작 시 1회) myiamInit({ serviceUid: 'YOUR_SERVICE_UID', oauth2ClientId: 'YOUR_CLIENT_ID', }) // 2. 로그인 window.location.href = await myiamLoginUrl('https://example.com/callback') // 3. 콜백 페이지에서 토큰 수신 const tokens = await myiamHandleCallback() // 4. 토큰 갱신 const newTokens = await myiamRefreshToken(tokens.refresh_token) // 5. 로그아웃 window.location.href = myiamLogoutUrl('https://example.com') ``` ### 인증 플로우 ```mermaid flowchart TD A["myiamLoginUrl()
로그인 URL 생성 + PKCE 값 저장"] A --> B["MyIAM 서버 로그인
사용자가 로그인 후 callbackUrl로 리다이렉트"] B --> C["myiamHandleCallback()
인가 코드를 토큰으로 교환"] C --> D["myiamRefreshToken()
액세스 토큰 만료 시 갱신"] D --> E["myiamLogoutUrl()
로그아웃 URL로 리다이렉트"] ``` ## 함수 ### [함수] myiamInit SDK를 초기화합니다. 다른 함수를 호출하기 전에 반드시 1회 호출해야 합니다. `시그니처` ```typescript myiamInit(config: MyiamConfig): void ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | config * | MyiamConfig | SDK 설정 객체 | `예시` ```typescript myiamInit({ serviceUid: 'YOUR_SERVICE_UID', oauth2ClientId: 'YOUR_CLIENT_ID', }) ``` ### [함수] myiamLoginUrl 로그인 페이지 URL을 생성합니다. PKCE 값(codeVerifier, state)을 자동으로 생성하고 저장합니다. `시그니처` ```typescript myiamLoginUrl(callbackUrl: string, options?: MyiamLoginOptions): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | callbackUrl * | string | 인증 완료 후 리다이렉트될 URL | | options | MyiamLoginOptions | 옵션 객체 | **반환값:** `Promise` — 리다이렉트할 로그인 URL `예시` ```typescript // 기본 window.location.href = await myiamLoginUrl('https://example.com/callback') // 옵션 지정 window.location.href = await myiamLoginUrl('https://example.com/callback', { state: 'custom-state', lang: 'en', }) ``` ### [함수] myiamSignupUrl 회원가입 페이지 URL을 생성합니다. 동작 방식은 myiamLoginUrl과 동일합니다. `시그니처` ```typescript myiamSignupUrl(callbackUrl: string, options?: MyiamSignupOptions): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | callbackUrl * | string | 인증 완료 후 리다이렉트될 URL | | options | MyiamSignupOptions | 옵션 객체 | **반환값:** `Promise` — 리다이렉트할 회원가입 URL `예시` ```typescript window.location.href = await myiamSignupUrl('https://example.com/callback') ``` ### [함수] myiamLogoutUrl 로그아웃 URL을 생성합니다. 동기 함수입니다. `시그니처` ```typescript myiamLogoutUrl(logoutRedirectUri: string, options?: MyiamLogoutOptions): string ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | logoutRedirectUri * | string | 로그아웃 후 리다이렉트될 URL | | options | MyiamLogoutOptions | 옵션 객체 | **반환값:** `string` — 리다이렉트할 로그아웃 URL `예시` ```typescript window.location.href = myiamLogoutUrl(window.location.origin) ``` ### [함수] myiamHandleCallback OAuth2 콜백을 처리합니다. URL에서 code와 state를 추출하고, PKCE state를 검증한 뒤, 인가 코드를 토큰으로 교환합니다. `시그니처` ```typescript myiamHandleCallback(searchParams?: string): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | searchParams | string | 쿼리 문자열. 생략 시 window.location.search 사용 | **반환값:** `Promise` — 액세스 토큰, 리프레시 토큰 등을 포함한 응답 #### Error Responses | HTTP 상태 | _code | 설명 | | --- | --- | --- | | 0 | authorization_error | 인증 서버가 `?error=`로 거절 (Redirect Url 불일치 등) | | 0 | manual_signup_unsupported | 수동 가입이 켜진 서비스라 콜백이 `?token=`으로 도착 | | 0 | missing_code | URL에 code 파라미터가 없음 | | 0 | missing_pkce_state | 저장된 PKCE state가 없음 (30분 경과, 미시작, 다른 탭이 덮어씀) | | 0 | state_mismatch | state 파라미터 불일치 (CSRF 의심) | 로그인과 회원가입이 같은 콜백으로 끝납니다 — 가입이 완료되면 서버가 같은 인증 요청을 이어받아 자동으로 로그인시키고 인가 코드를 들려 보내므로, 가입용 콜백을 따로 만들 필요가 없습니다. #### Error Responses 전부 [`MyiamCallbackError`](#myiamcallbackerror)로 throw됩니다. `예시` ```typescript try { const tokens = await myiamHandleCallback() sessionStorage.setItem('access_token', tokens.access_token) sessionStorage.setItem('refresh_token', tokens.refresh_token) } catch (err) { if (err instanceof MyiamCallbackError) { console.error(err.code, err.message) } } ``` ### [함수] myiamRefreshToken 리프레시 토큰으로 새 토큰을 발급받습니다. `시그니처` ```typescript myiamRefreshToken(refreshToken: string): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | refreshToken * | string | 기존 리프레시 토큰 | **반환값:** `Promise` — 새 액세스 토큰과 리프레시 토큰 `예시` ```typescript const newTokens = await myiamRefreshToken(currentRefreshToken) sessionStorage.setItem('access_token', newTokens.access_token) sessionStorage.setItem('refresh_token', newTokens.refresh_token) ``` ## 타입 ### [타입] MyiamConfig myiamInit에 전달하는 설정 객체입니다. ```typescript interface MyiamConfig { serviceUid: string oauth2ClientId: string myiamAppUrl?: string lang?: string } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | serviceUid * | string | 서비스 고유 식별자 | | oauth2ClientId * | string | OAuth2 클라이언트 ID | | myiamAppUrl | string | 인증 서버 주소. 셀프 호스팅·테스트 환경에서만 지정 | | lang | string | 로그인·회원가입·로그아웃 등 모든 인증 페이지의 기본 표시 언어. 함수별 `options.lang`으로 덮어쓸 수 있습니다 | ### [타입] MyiamLoginOptions myiamLoginUrl에 전달하는 옵션 객체입니다. ```typescript interface MyiamLoginOptions { state?: string lang?: string } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | state | string | CSRF 방지용 state 값. 생략 시 32자 랜덤 문자열 자동 생성 | | lang | string | 인증 페이지 표시 언어 (`en`, `ko`, `ja`, `zh` 등). 생략 시 브라우저 `Accept-Language` 기준. `MyiamConfig.lang`보다 우선합니다 | ### [타입] MyiamSignupOptions myiamSignupUrl에 전달하는 옵션 객체입니다. ```typescript interface MyiamSignupOptions { state?: string lang?: string } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | state | string | CSRF 방지용 state 값. 생략 시 32자 랜덤 문자열 자동 생성 | | lang | string | 인증 페이지 표시 언어 (`en`, `ko`, `ja`, `zh` 등). 생략 시 브라우저 `Accept-Language` 기준. `MyiamConfig.lang`보다 우선합니다 | ### [타입] MyiamLogoutOptions myiamLogoutUrl에 전달하는 옵션 객체입니다. ```typescript interface MyiamLogoutOptions { lang?: string } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | lang | string | 인증 페이지 표시 언어 (`en`, `ko`, `ja`, `zh` 등). 생략 시 브라우저 `Accept-Language` 기준. `MyiamConfig.lang`보다 우선합니다 | ### [타입] MyiamTokenResponse 토큰 교환 및 갱신 시 반환되는 응답 객체입니다. ```typescript interface MyiamTokenResponse { access_token: string refresh_token: string token_type: string expires_in: number } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | access_token * | string | 액세스 토큰 | | refresh_token * | string | 리프레시 토큰 | | token_type * | string | 토큰 타입 (예: "Bearer") | | expires_in * | number | 액세스 토큰 만료 시간 (초) | ### [타입] MyiamCallbackError myiamHandleCallback에서 발생하는 에러 클래스입니다. Error를 상속합니다. ```typescript class MyiamCallbackError extends Error { readonly code: MyiamCallbackErrorCode readonly name: "MyiamCallbackError" } ``` #### 에러 코드 | 필드 | 설명 | 대응 | | --- | --- | --- | | `authorization_error` | 인증 서버가 `?error=`로 거절했습니다. 메시지에 `error_description`이 담깁니다 | 대개 Redirect Url 등록값과 앱이 보낸 주소가 다릅니다. OAuth2 설정을 확인하세요 | | `manual_signup_unsupported` | 수동 가입이 켜진 서비스라 콜백이 `?token=`으로 왔습니다 | 완료 처리에 API 키가 필요해 브라우저에서는 끝낼 수 없습니다. [Server SDK](https://myiam.io/docs/sdk/web/server)로 처리하거나 수동 가입을 끕니다 | | `missing_code` | 콜백 URL에 code 파라미터가 없음 | 콜백 주소로 직접 들어온 경우입니다. 첫 화면으로 보냅니다 | | `missing_pkce_state` | 저장된 PKCE state를 찾을 수 없음 | 30분이 지났거나 다른 탭이 덮어썼습니다. 다시 로그인시킵니다 | | `state_mismatch` | state 값 불일치 (CSRF 공격 가능성) | 다시 로그인시킵니다 | `에러 처리 예시` ```typescript import { myiamHandleCallback, MyiamCallbackError } from '@myiam.io/web-sdk/client' try { const tokens = await myiamHandleCallback() } catch (err) { if (err instanceof MyiamCallbackError) { switch (err.code) { case 'missing_code': // 인가 코드 없음 → 홈으로 이동 break case 'state_mismatch': // CSRF 의심 → 로그인 재시작 break case 'missing_pkce_state': // 세션 만료 → 로그인 재시작 break case 'authorization_error': case 'manual_signup_unsupported': // 서비스 설정 문제 → 재시도해도 같으므로 안내 화면으로 break } } } ``` --- ## Web SDK Server API 레퍼런스 URL: https://myiam.io/docs/sdk/web/server @myiam.io/web-sdk/server - 서버 환경을 위한 세션 관리 및 HTTP API SDK 문서. 패키지: https://www.npmjs.com/package/@myiam.io/web-sdk 릴리스 노트: https://myiam.io/docs/release-notes/web-sdk # Server SDK API 레퍼런스 서버 환경(Node.js, Edge Runtime)을 위한 세션 관리 및 HTTP API SDK. 외부 의존성 없이 동작하며, 쿠키 기반 세션 관리·MyIAM HTTP API 호출을 지원합니다. ## 시작 가이드 설치, 빠른 시작, Next.js 사용 예시 ### 설치 ```bash npm install @myiam.io/web-sdk ``` ### 빠른 시작 ```typescript import { createMyiamServer } from '@myiam.io/web-sdk/server' const myiam = createMyiamServer({ serviceUid: 'YOUR_SERVICE_UID', oauth2ClientId: 'YOUR_CLIENT_ID', cookieSecret: 'YOUR_COOKIE_SECRET_32_CHARS_MIN', apiKey: 'YOUR_API_KEY', // HTTP API 호출 시 필요 }) ``` #### Next.js에서 사용 `Next.js 예시` ```typescript // lib/myiam.ts import { createMyiamServer } from '@myiam.io/web-sdk/server' export const myiam = createMyiamServer({ serviceUid: process.env.SERVICE_UID!, oauth2ClientId: process.env.OAUTH2_CLIENT_ID!, cookieSecret: process.env.COOKIE_SECRET!, apiKey: process.env.API_KEY!, }) // app/api/login/route.ts import { myiam } from '@/lib/myiam' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' export async function GET() { const url = await myiam.login('https://example.com/callback', await cookies()) redirect(url) } // app/dashboard/page.tsx (Server Component) import { myiam } from '@/lib/myiam' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' export default async function DashboardPage() { const session = await myiam.getSession(await cookies()) if (!session) redirect('/') const user = await myiam.api.getUser({ accessToken: session.accessToken, }) return

{user.username}

} ``` ## 초기화 ### [함수] createMyiamServer 서버 인스턴스를 생성합니다. Factory 패턴으로, 글로벌 상태 없이 동작합니다. `시그니처` ```typescript createMyiamServer(config: MyiamServerConfig): MyiamServer ``` ### [타입] MyiamServerConfig createMyiamServer에 전달하는 설정 객체입니다. ```typescript interface MyiamServerConfig { serviceUid: string oauth2ClientId: string cookieSecret: string apiKey?: string myiamAppUrl?: string myiamApiUrl?: string cookieName?: string cookieOptions?: CookieOptions lang?: string } ``` #### 필드 | 속성명 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | serviceUid * | string | | 서비스 고유 식별자 | | oauth2ClientId * | string | | OAuth2 클라이언트 ID | | cookieSecret * | string | | 세션 암호화 키 (32자 이상 권장) | | apiKey | string | | HTTP API 호출용 API 키 | | myiamAppUrl | string | https://app.myiam.io | 인증 서버 주소. 셀프 호스팅·테스트 환경에서만 지정 | | myiamApiUrl | string | https://api.myiam.io | REST API 주소. 셀프 호스팅·테스트 환경에서만 지정 | | cookieName | string | __myiam_session | 세션 쿠키 이름 | | cookieOptions | CookieOptions | | 쿠키 옵션 | | lang | string | | 인증 페이지 표시 언어 (`en`, `ko`, `ja`, `zh` 등). 로그인·회원가입·로그아웃 URL과 `prepareUserAction`(회원 탈퇴, 프로필 수정 등)에 모두 적용됩니다. 생략 시 브라우저 `Accept-Language` 기준 | `CookieOptions 기본값` ```typescript { httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 604800, } ``` `maxAge`는 **세션 쿠키에만** 적용됩니다. `login()`· `signup()`이 따로 심는 PKCE 쿠키(`_pkce`)는 **30분** 고정으로, 서버가 가입 절차에 허용하는 시간과 같습니다. 약관 동의 → 정보 입력 → 이메일 인증 → 가입 후 자동 로그인이 모두 이 안에서 끝나야 하며, 넘기면 콜백에 인가 코드가 정상 도착해도 교환할 PKCE 값이 남아 있지 않아 `missing_pkce_state`로 실패합니다. `secure: true`가 기본이라 쿠키는 HTTPS에서만 전송됩니다. Chrome·Firefox는 `http://localhost`를 예외로 두지만 **Safari는 예외를 두지 않아** 로컬 개발 중 세션·PKCE 쿠키가 조용히 버려집니다. Safari로 `http://localhost`를 테스트한다면 개발 환경에서만 `cookieOptions: { secure: false }`를 주세요. ### [함수] createMyiamApi API 전용 클라이언트를 생성합니다. 세션/쿠키 없이 HTTP API만 호출할 때 사용합니다 (예: Firebase Functions, CLI 도구, 백엔드 서비스). `시그니처` ```typescript createMyiamApi(config: MyiamApiConfig): MyiamApi ``` `예시` ```typescript import { createMyiamApi } from '@myiam.io/web-sdk/server' const api = createMyiamApi({ serviceUid: 'YOUR_SERVICE_UID', apiKey: 'YOUR_API_KEY', }) // 세션/쿠키 없이 API만 호출 const info = await api.getTokenInfo(accessToken) const user = await api.getUser({ accessToken }) ``` REST API만 필요하다면 **`@myiam.io/web-sdk/api`** 진입점을 쓰는 편이 낫습니다 (0.5.5+). 내보내는 값은 `createMyiamApi`· `MyiamApiError`와 관련 타입으로 동일하지만, 세션·쿠키·암호화 코드가 번들에 포함되지 않고 ESM/CJS 양쪽으로 제공됩니다. ```typescript import { createMyiamApi, MyiamApiError } from '@myiam.io/web-sdk/api' ``` Expo SDK의 `useMyiam().api`도 이 진입점을 그대로 재사용하므로 메서드 시그니처가 동일합니다. ### [타입] MyiamApiConfig createMyiamApi에 전달하는 설정 객체입니다. ```typescript interface MyiamApiConfig { serviceUid: string apiKey: string myiamApiUrl?: string lang?: string } ``` #### 필드 | 속성명 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | serviceUid * | string | | 서비스 고유 식별자 | | apiKey * | string | | HTTP API 호출용 API 키 | | myiamApiUrl | string | https://api.myiam.io | REST API 주소. 셀프 호스팅·테스트 환경에서만 지정 | | lang | string | | `prepareUserAction`으로 이동할 페이지의 기본 표시 언어 | ## 세션 관리 ### [함수] login PKCE 인증 URL을 생성합니다. codeVerifier와 state를 암호화 쿠키에 저장합니다. `시그니처` ```typescript myiam.login(callbackUrl: string, cookies: CookieStore, options?: { state?: string; lang?: string }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | callbackUrl * | string | 인증 완료 후 리다이렉트될 URL | | cookies * | CookieStore | 쿠키 저장소 | | options.state | string | CSRF 방지용 state 값. 생략 시 자동 생성 | | options.lang | string | 인증 페이지 표시 언어. `MyiamServerConfig.lang`보다 우선 | **반환값:** `Promise` — 리다이렉트할 로그인 URL `예시` ```typescript // Next.js Route Handler import { cookies } from 'next/headers' import { redirect } from 'next/navigation' export async function GET() { const url = await myiam.login('https://example.com/callback', await cookies()) redirect(url) } ``` ### [함수] signup PKCE 회원가입 URL을 생성합니다. login과 동일한 PKCE 흐름을 사용하며, 회원가입 화면으로 이동합니다. `시그니처` ```typescript myiam.signup(callbackUrl: string, cookies: CookieStore, options?: { state?: string; lang?: string }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | callbackUrl * | string | 인증 완료 후 리다이렉트될 URL | | cookies * | CookieStore | 쿠키 저장소 | | options.state | string | CSRF 방지용 state 값. 생략 시 자동 생성 | | options.lang | string | 인증 페이지 표시 언어. `MyiamServerConfig.lang`보다 우선 | **반환값:** `Promise` — 리다이렉트할 회원가입 URL `예시` ```typescript // Next.js Route Handler import { cookies } from 'next/headers' import { redirect } from 'next/navigation' export async function GET() { const url = await myiam.signup('https://example.com/callback', await cookies()) redirect(url) } ``` ### [함수] handleCallback OAuth2 콜백을 처리합니다. PKCE 쿠키에서 codeVerifier를 읽고, state를 검증한 뒤, 인가 코드를 토큰으로 교환합니다. 세션을 암호화 쿠키에 저장합니다. `시그니처` ```typescript myiam.handleCallback(searchParams: string | URLSearchParams, cookies: CookieStore): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | searchParams * | string \| URLSearchParams | 콜백 URL의 쿼리 파라미터 | | cookies * | CookieStore | 쿠키 저장소 | **반환값:** `Promise` — 세션 정보 로그인과 회원가입이 같은 콜백으로 끝납니다 — 가입이 완료되면 서버가 같은 인증 요청을 이어받아 자동으로 로그인시키고 인가 코드를 들려 보내므로, 가입용 콜백을 따로 만들 필요가 없습니다. 실패하면 [`MyiamCallbackError`](#myiamcallbackerror)를 throw합니다. `code`로 "다시 로그인하면 되는 실패"와 "설정이 잘못되어 몇 번을 해도 실패하는 것"이 갈리므로, 콜백 핸들러는 반드시 감싸 주세요. `예시` ```typescript // Next.js Route Handler import { MyiamCallbackError } from '@myiam.io/web-sdk/server' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { type NextRequest } from 'next/server' export async function GET(request: NextRequest) { try { await myiam.handleCallback(request.nextUrl.searchParams, await cookies()) } catch (err) { if (err instanceof MyiamCallbackError) { console.error('[myiam] callback failed:', err.code, err.message) redirect(`/?error=${err.code}`) } throw err } // redirect()는 예외를 던져 동작하므로 try 밖에 둡니다 redirect('/dashboard') } ``` **수동 가입은 이 함수가 처리하지 않습니다.** 서비스 > 서비스 정보의 **수동 가입**이 켜져 있으면 콜백이 인가 코드 대신 가입 토큰(`?token=`)을 들고 돌아옵니다. `handleCallback()`은 이때 `manual_signup_unsupported`로 실패하므로, 가입을 [`completeRegistration`](#completeregistration)으로 확정하고 응답의 `redirect_url`로 사용자를 보내 인가 코드를 받아야 합니다. ### [함수] getSession 세션 쿠키를 복호화하여 현재 세션을 반환합니다. `시그니처` ```typescript myiam.getSession(cookies: ReadonlyCookieStore): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | cookies * | ReadonlyCookieStore | 쿠키 저장소 (읽기 전용) | **반환값:** `Promise` — 세션이 없거나 복호화 실패 시 null `예시` ```typescript // Next.js Server Component import { cookies } from 'next/headers' const session = await myiam.getSession(await cookies()) if (!session) { redirect('/') } ``` ### [함수] refreshSession 세션의 리프레시 토큰으로 새 토큰을 발급받고 세션 쿠키를 갱신합니다. `시그니처` ```typescript myiam.refreshSession(cookies: CookieStore): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | cookies * | CookieStore | 쿠키 저장소 | **반환값:** `Promise` — 갱신 실패 시 null ### [함수] logout 세션 쿠키를 삭제하고 로그아웃 URL을 반환합니다. `시그니처` ```typescript myiam.logout(redirectUri: string, cookies: CookieStore, options?: { lang?: string }): string ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | redirectUri * | string | 로그아웃 후 리다이렉트될 URL | | cookies * | CookieStore | 쿠키 저장소 | | options.lang | string | 로그아웃 페이지 표시 언어. `MyiamServerConfig.lang`보다 우선 | **반환값:** `string` — 리다이렉트할 로그아웃 URL `예시` ```typescript // Next.js Route Handler import { cookies } from 'next/headers' import { redirect } from 'next/navigation' export async function GET() { const url = myiam.logout('https://example.com', await cookies()) redirect(url) } ``` ## HTTP API (myiam.api) HTTP API는 myiam.api 네임스페이스를 통해 호출합니다. apiKey 설정이 필요합니다. ### 회원 API ### [함수] completePreparation 회원 가입 준비를 완료하고 다음 단계 리다이렉트 URL을 반환합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#account-prepare-complete) `시그니처` ```typescript myiam.api.completePreparation(params: { token: string profile_data?: UserProfiles }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | string | 가입 준비 콜백 URL로 전달된 토큰 | | profile_data | UserProfiles | 사용자 프로필 데이터 (이름, 이메일, custom_fields 등) | `예시` ```typescript // 토큰만 전달 const result = await myiam.api.completePreparation({ token }) redirect(result.redirect_url) // 프로필 데이터와 함께 전달 const result = await myiam.api.completePreparation({ token, profile_data: { name: '홍길동', email: 'user@example.com', custom_fields: { company: 'MyIAM' }, }, }) redirect(result.redirect_url) ``` ### [함수] completeRegistration 수동 회원 가입을 완료합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#account-register-complete) `시그니처` ```typescript myiam.api.completeRegistration(params: { token: string custom_fields?: Record }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | string | 수동 회원 가입 콜백 URL로 전달된 서비스 가입 토큰 | | custom_fields | Record\ | 사용자 정의 필드 | `예시` ```typescript const result = await myiam.api.completeRegistration({ token }) console.log(result.user.uid, result.user.profiles.email) // 가입 완료 후 리다이렉트 redirect(result.redirect_url) // custom_fields와 함께 전달 const result = await myiam.api.completeRegistration({ token, custom_fields: { company: 'MyIAM', role: 'admin' }, }) redirect(result.redirect_url) ``` ### [함수] completeDeregistration 수동 회원 탈퇴를 완료합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#account-deregister-complete) `시그니처` ```typescript myiam.api.completeDeregistration(params: { token: string type?: string reason?: string message?: string }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | string | 탈퇴 토큰 | | type | string | 탈퇴 이유 타입 | | reason | string | 탈퇴 이유 | | message | string | 탈퇴 메시지 | `예시` ```typescript const result = await myiam.api.completeDeregistration({ token, type: 'NOT_USEFUL', reason: '서비스를 더 이상 이용하지 않습니다.', }) redirect(result.redirect_url) ``` ### 토큰 API ### [함수] getTokenInfo Access Token의 유효성 및 정보를 조회합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#token-info) `시그니처` ```typescript myiam.api.getTokenInfo(accessToken: string): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | string | 조회할 Access Token | `예시` ```typescript const session = await myiam.getSession(await cookies()) const info = await myiam.api.getTokenInfo(session!.accessToken) if (!info.active) { // 토큰 만료 → 갱신 또는 재로그인 } ``` ### [함수] deleteToken Access Token을 만료 처리합니다. target 파라미터로 삭제 범위를 지정할 수 있습니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#token-delete) `시그니처` ```typescript myiam.api.deleteToken(params: { accessToken: string serviceUserUid?: string target?: "ACCOUNT_USER" | "SERVICE" refreshTokenDelete?: boolean }): Promise ``` #### 매개변수 | 속성명 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | accessToken * | string | | 삭제할 Access Token | | serviceUserUid | string | | 서비스 사용자 UID | | target | string | | 삭제 범위 | | refreshTokenDelete | boolean | true | Refresh Token도 함께 삭제할지 여부 | #### target 값 | 속성명 | 설명 | | --- | --- | | (미지정) | 현재 Access Token만 삭제 | | ACCOUNT_USER | 해당 계정 사용자의 모든 토큰 삭제 | | SERVICE | 서비스 전체 사용자의 모든 토큰 삭제 | `예시` ```typescript // 현재 토큰만 삭제 await myiam.api.deleteToken({ accessToken: session.accessToken }) // 해당 사용자의 모든 토큰 삭제 await myiam.api.deleteToken({ accessToken: session.accessToken, serviceUserUid: user.uid, target: 'ACCOUNT_USER', }) ``` ### 사용자 API ### [함수] getUser 현재 인증된 사용자의 정보를 조회합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#user-me) `시그니처` ```typescript myiam.api.getUser(params: { accessToken: string serviceUserUid?: string }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | string | Access Token | | serviceUserUid | string | 서비스 사용자 UID | `예시` ```typescript const session = await myiam.getSession(await cookies()) const user = await myiam.api.getUser({ accessToken: session!.accessToken }) console.log(user.username) // "user@example.com" ``` ### [함수] prepareUserAction 특정 사용자 동작을 위한 임시 토큰을 발급하고, 해당 동작 화면으로 이동할 수 있는 리다이렉트 URL을 반환합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#user-prepare-type) `시그니처` ```typescript myiam.api.prepareUserAction( type: PrepareActionType, params: { accessToken: string serviceUserUid: string completeRedirectUrl: string state?: string editParams?: { fields?: string[] } lang?: string }, ): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | type * | PrepareActionType | 동작 타입 | | accessToken * | string | Access Token | | serviceUserUid * | string | 서비스 사용자 UID | | completeRedirectUrl * | string | 동작 완료 후 리다이렉트할 URL | | state | string | 완료 후 전달받을 임의 상태값 | | editParams.fields | string[] | edit-profile 타입에서 수정 가능한 필드 목록 | | lang | string | 이동할 페이지의 표시 언어. 서버가 `redirect_url`에 반영합니다. `MyiamServerConfig.lang`보다 우선 | 반환된 `redirect_url`에 포함된 토큰은 **15분** 동안 유효합니다. #### 동작 타입 (PrepareActionType) | 속성명 | 설명 | | --- | --- | | deregister | 회원 탈퇴 | | set-password | 비밀번호 설정 | | reset-password | 비밀번호 재설정 | | edit-profile | 프로필 수정 | | edit-email | 이메일 수정 | | passkey | 패스키(Passkey) 관리 | `예시` ```typescript // 비밀번호 재설정 화면으로 이동 const result = await myiam.api.prepareUserAction('reset-password', { accessToken: session.accessToken, serviceUserUid: user.uid, completeRedirectUrl: 'https://example.com/settings', }) redirect(result.redirect_url) // 프로필 수정 (특정 필드만 수정 가능하게) const result = await myiam.api.prepareUserAction('edit-profile', { accessToken: session.accessToken, serviceUserUid: user.uid, completeRedirectUrl: 'https://example.com/settings', editParams: { fields: ['nickname', 'mobile_number'] }, }) redirect(result.redirect_url) ``` ### 서비스 사용자 API ### [함수] getServiceUser 현재 인증된 서비스 사용자의 기본 정보를 조회합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#service-user-get) `시그니처` ```typescript myiam.api.getServiceUser(params: { accessToken: string serviceUserUid: string }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | string | Access Token | | serviceUserUid * | string | 서비스 사용자 UID | `예시` ```typescript const serviceUser = await myiam.api.getServiceUser({ accessToken: session.accessToken, serviceUserUid: user.uid, }) console.log(serviceUser.status, serviceUser.created_at) ``` ### [함수] getServiceUserProfile 현재 인증된 서비스 사용자의 프로필 정보를 조회합니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#service-user-profile-get) `시그니처` ```typescript myiam.api.getServiceUserProfile(params: { accessToken: string serviceUserUid: string }): Promise ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | string | Access Token | | serviceUserUid * | string | 서비스 사용자 UID | `예시` ```typescript const profile = await myiam.api.getServiceUserProfile({ accessToken: session.accessToken, serviceUserUid: user.uid, }) console.log(profile.data.email, profile.data.name) ``` ## 타입 ### [타입] MyiamSession 세션 정보 객체입니다. ```typescript interface MyiamSession { accessToken: string refreshToken: string expiresAt: number // Unix timestamp (ms) } ``` ### [타입] CookieStore / ReadonlyCookieStore 쿠키 저장소 인터페이스입니다. Next.js cookies() API와 호환됩니다. ```typescript interface ReadonlyCookieStore { get(name: string): { name: string; value: string } | undefined } interface CookieStore extends ReadonlyCookieStore { set(name: string, value: string, options?: Record): void delete(name: string): void } ``` ### [타입] MyiamCallbackError handleCallback() 실패 시 throw되는 에러 클래스입니다. code로 원인을 구분합니다. ```typescript class MyiamCallbackError extends Error { readonly code: MyiamCallbackErrorCode readonly message: string } ``` | `code` | 뜻 | 대응 | | --- | --- | --- | | `authorization_error` | 인증 서버가 `?error=`로 거절했습니다. 메시지에 `error_description`이 담깁니다 | 대개 Redirect Url 등록값과 앱이 보낸 주소가 다릅니다. OAuth2 설정을 확인하세요 | | `manual_signup_unsupported` | 수동 가입이 켜진 서비스라 콜백이 `?token=`으로 왔습니다 | `completeRegistration()`으로 가입을 확정하고 응답의 `redirect_url`로 보내거나, 수동 가입을 끕니다 | | `missing_code` | `code`도 `error`도 `token`도 없습니다 | 콜백 주소로 직접 들어온 경우입니다. 첫 화면으로 보냅니다 | | `missing_pkce_state` | PKCE 쿠키가 없습니다. 30분이 지났거나, 다른 탭이 덮어썼거나, 브라우저가 쿠키를 버렸습니다 | 다시 로그인시킵니다. 반복되면 `secure` 쿠키 주의사항을 확인하세요 | | `invalid_pkce_state` | PKCE 쿠키를 복호화하지 못했습니다 | `cookieSecret`이 바뀌었거나 인스턴스마다 다릅니다. 서버 전체가 같은 값을 쓰는지 확인하세요 | | `state_mismatch` | 돌아온 `state`가 보낸 값과 다릅니다 | 다시 로그인시킵니다. 사용자가 오래된 콜백 URL을 다시 연 경우가 대부분입니다 | ### [타입] MyiamApiError HTTP API 호출 실패 시 throw되는 에러 클래스입니다. ```typescript class MyiamApiError extends Error { readonly status: number // HTTP 상태 코드 readonly code: number // MyIAM _code 값 readonly message: string } ``` `에러 처리 예시` ```typescript import { MyiamApiError } from '@myiam.io/web-sdk/server' try { const info = await myiam.api.getTokenInfo(accessToken) } catch (err) { if (err instanceof MyiamApiError) { console.error(err.status, err.code, err.message) // 401, 401, "Unauthorized" } } ``` ### [타입] PrepareCompleteResponse 회원 가입 준비 완료 응답입니다. ```typescript interface PrepareCompleteResponse { redirect_url: string } ``` ### [타입] RegisterCompleteResponse 회원 가입 완료 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#account-register-complete) ```typescript interface RegisterCompleteResponse { redirect_url: string user: { uid: string status: string // "NORMAL" | "UNUSED" | "BLOCKED" | "DEREGISTERED" created_at: string profiles: UserProfiles } } ``` ### [타입] DeregisterCompleteResponse 회원 탈퇴 완료 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#account-deregister-complete) ```typescript interface DeregisterCompleteResponse { redirect_url: string service_user_uid: string } ``` ### [타입] TokenInfoResponse 토큰 유효성 조회 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#token-info) ```typescript interface TokenInfoResponse { active: boolean expired_at?: string service_user_uid?: string } ``` ### [타입] UserMeResponse 사용자 정보 조회 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#user-me) ```typescript interface UserMeResponse { uid: string service_uid: string username: string authority: Array> } ``` ### [타입] PrepareActionResponse 사용자 동작 준비 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#user-prepare-type) ```typescript interface PrepareActionResponse { redirect_url: string } ``` ### [타입] ServiceUserResponse 서비스 사용자 기본 정보 조회 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#service-user-get) ```typescript interface ServiceUserResponse { id?: number uid: string service_id?: number status: string visited_at?: string created_at: string updated_at?: string } ``` ### [타입] ServiceUserProfileResponse 서비스 사용자 프로필 조회 응답입니다. [HTTP API 레퍼런스 보기](https://myiam.io/docs/api#service-user-profile-get) ```typescript interface ServiceUserProfileResponse { service_user_uid: string service_user_id?: number status: string data: { email?: string | null name?: string | null nickname?: string | null gender?: string | null birth_year?: string | null birthday?: string | null date_of_birth?: string | null mobile_number?: string | null home_number?: string | null phone_number?: string | null address?: { address1?: string | null address2?: string | null zipcode?: string | null } | null custom_fields?: Record | null } created_at: string updated_at?: string } ``` ### [타입] UserProfiles 사용자 프로필 필드 인터페이스입니다. RegisterCompleteResponse 및 ServiceUserProfileResponse에서 사용됩니다. ```typescript interface UserProfiles { email?: string | null name?: string | null nickname?: string | null gender?: string | null birth_year?: string | null birthday?: string | null date_of_birth?: string | null mobile_number?: string | null home_number?: string | null phone_number?: string | null address?: { address1?: string | null address2?: string | null zipcode?: string | null } | null custom_fields?: Record | null } ``` --- ## Flutter SDK API 레퍼런스 URL: https://myiam.io/docs/sdk/flutter myiam_flutter_sdk — Flutter 앱을 위한 OAuth2/PKCE 인증, REST API SDK 문서. 패키지: https://pub.dev/packages/myiam_flutter_sdk 릴리스 노트: https://myiam.io/docs/release-notes/flutter-sdk # Flutter SDK Flutter 앱을 위한 OAuth2/PKCE 인증, REST API SDK. [Auth SDKRiverpod 기반 OAuth2/PKCE 인증 상태 관리. 로그인·회원가입·로그아웃 스크린 및 위젯을 제공합니다.](https://myiam.io/docs/sdk/flutter/auth) [REST APIMyIAM REST API 클라이언트. 토큰 관리, 사용자 조회, 회원 가입/탈퇴 완료 등을 지원합니다.](https://myiam.io/docs/sdk/flutter/rest-api) [Firebase 연동MyIAM access_token을 Firebase Custom Token으로 교환해 Firestore·Storage 등 Firebase 서비스에 연결합니다.](https://myiam.io/docs/sdk/flutter/firebase) [Supabase 연동Edge Function으로 Supabase 세션을 발급받아 Database·Storage의 RLS 접근 제어와 연결합니다.](https://myiam.io/docs/sdk/flutter/supabase) --- ## Flutter SDK Auth API 레퍼런스 URL: https://myiam.io/docs/sdk/flutter/auth myiam_flutter_sdk - Flutter 앱을 위한 OAuth2/PKCE 인증 SDK 문서. 패키지: https://pub.dev/packages/myiam_flutter_sdk 릴리스 노트: https://myiam.io/docs/release-notes/flutter-sdk # Flutter SDK Auth API 레퍼런스 Flutter 앱을 위한 OAuth2/PKCE 인증 SDK. 로그인·회원가입·로그아웃·토큰 갱신·사용자 액션을 지원합니다. ## 시작 가이드 설치, 빠른 시작, 인증 플로우 ### 설치 ```bash flutter pub add myiam_flutter_sdk ``` ### 빠른 시작 ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; // 1. 초기화 (ProviderScope에서 설정) void main() { runApp( ProviderScope( overrides: [ authConfigProvider.overrideWithValue( const MyiamConfig( serviceUid: 'YOUR_SERVICE_UID', oauth2ClientId: 'YOUR_CLIENT_ID', apiKey: 'YOUR_API_KEY', redirectScheme: 'myiamsample', redirectHost: 'oauth2callback', ), ), ], child: const MyApp(), ), ); } // 2. 로그인 LoginScreen( onLogin: (tokens) async { final api = ref.read(myiamApiProvider); final userMe = await api.getUser(accessToken: tokens.accessToken); return MyiamUser( uid: userMe.uid, serviceUid: userMe.serviceUid, username: userMe.username, authority: userMe.authority, ); }, ) // 3. 인증 상태 확인 final authState = ref.watch(authNotifierProvider); // 4. 토큰 갱신 await ref.read(authNotifierProvider.notifier).refreshToken(); // 5. 로그아웃 LogoutScreen() ``` ### 환경 변수 `flutter run --dart-define-from-file=env.json`으로 환경 변수를 전달합니다. `env.json` ```json { "SERVICE_UID": "", "OAUTH2_CLIENT_ID": "", "API_KEY": "", "REDIRECT_SCHEME": "myiamsample", "REDIRECT_HOST": "oauth2callback", "SIGNUP_REDIRECT_SCHEME": "myiamsample", "SIGNUP_REDIRECT_HOST": "signup-callback", "DEREGISTER_REDIRECT_SCHEME": "myiamsample", "DEREGISTER_REDIRECT_HOST": "deregister-callback" } ``` ### 인증 플로우 ```mermaid flowchart TD A["ProviderScope에서 SDK 초기화
authConfigProvider를 오버라이드하여 서비스 설정 주입"] A --> B["LoginScreen / SignupScreen
SDK가 WebView에서 인증 화면을 표시하고 OAuth2 PKCE 처리"] B --> C["onLogin / onSignup 콜백
토큰을 받아 사용자 정보를 조회하고 인증 상태 자동 전환"] C --> D["authNotifierProvider.refreshToken()
액세스 토큰 만료 시 갱신, 실패 시 자동 로그아웃"] D --> E["LogoutScreen / logout()
서버 로그아웃(WebView) 또는 로컬 세션 정리"] ``` ## WebView 모드 Custom Tab vs InAppWebView 비교, 위젯별 기본 모드 SDK는 두 가지 WebView 모드를 지원합니다. 모든 위젯에서 `mode` 매개변수로 변경할 수 있습니다. | | 시스템 브라우저 (Chrome Custom Tab) | 앱 내장 WebView | | --- | --- | --- | | 모드 옵션 | `customTab` | `inAppWebView` | | Passkey (FIDO2) | ✅ 지원 | ❌ 미지원 | | SNS 로그인 | ✅ 브라우저와 동일 | ⚠️ 원활하지 않을 수 있음 | | UX | ⚠️ 브라우저 UI 노출 | ✅ 앱 내 화면 | ### 위젯별 기본 모드 | 위젯 | 기본 모드 | | --- | --- | | `LoginScreen` / `LoginWebView` | `customTab` | | `SignupScreen` / `SignupWebView` | `inAppWebView` | | `LogoutScreen` / `LogoutWebView` | `inAppWebView` | | `MyiamAction` | `inAppWebView` | ### 모드 변경 `모드 변경 예시` ```dart // Passkey·SNS 로그인 미사용 서비스라면 로그인도 InAppWebView로 LoginScreen(onLogin: ..., mode: MyiamWebViewMode.inAppWebView) // Passkey를 쓴다면 사용자 액션은 CustomTab으로 MyiamAction.passkey(..., mode: MyiamWebViewMode.customTab) ``` ## 수동 회원가입 이메일 인증·관리자 승인으로 가입 확정을 분리하는 서비스용 설정 MyIAM 서비스가 **수동 회원가입 모드**(이메일 인증/관리자 승인 등으로 가입 확정을 분리)로 설정된 경우, 일반 OAuth2 authorize 콜백과는 **별개의 콜백**으로 `?token=...`이 전달됩니다. SDK가 이 콜백을 자동으로 처리하므로, 개발자는 콜백 URL만 추가 설정하면 `SignupScreen` / `SignupWebView`를 일반 가입과 동일하게 사용할 수 있습니다. ### 흐름 개요 ```text 1. SignupScreen 오픈 → MyIAM 가입 폼 2. 가입 제출 → MyIAM이 signupRedirectUri 로 ?token= 발송 3. SDK가 내부적으로 api.completeRegistration(token) 호출 4. 응답의 redirect_url 로 WebView/Custom Tab 재진입 · 자동로그인: 즉시 ?code=&state= 로 리턴 · 수동로그인: 로그인 폼 → 완료 후 ?code=&state= 5. SDK가 OAuth code → 토큰 교환 → onSignup 콜백 ``` ### 콜백 URL 분리 `MyiamConfig`에 수동가입 전용 URL을 별도로 지정합니다. 미지정 시 `redirectUri`로 fallback되지만, OAuth2 redirect_uri와 혼용되지 않도록 **분리를 권장**합니다. ```dart const MyiamConfig( // 일반 OAuth2 redirect_uri (login/signup authorize + token exchange 공용) redirectScheme: 'myapp', redirectHost: 'oauth2callback', // 수동가입 완료 알림 콜백 (MyIAM이 ?token= 을 여기로 발송) signupRedirectScheme: 'myapp', signupRedirectHost: 'signup-callback', ... ) ``` **중요:** `signupRedirectUri`는 수동가입 1차 콜백 수신 전용입니다. OAuth2 authorize/token 요청 어디에도 포함되지 않으며, MyIAM 콘솔에서는 "수동가입 콜백" 필드에만 등록합니다 (OAuth2 redirect_uri 목록에 추가하면 안 됩니다). ### 플랫폼별 설정 Android — `AndroidManifest.xml`에 두 콜백 모두 등록 ```xml ``` **iOS** — `Info.plist`는 scheme 단위로만 등록되므로 추가 작업 불필요 (두 host 모두 같은 scheme 사용 시). ## 수동 탈퇴 관리자 승인·비동기 탈퇴 확정이 필요한 서비스용 설정 수동 회원가입과 대칭되는 플로우입니다. MyIAM이 탈퇴 절차를 진행한 뒤 앱으로 `?token=...`이 포함된 별도의 앱 링크를 보내고, 앱은 그 token을 `api.completeDeregistration(token)`으로 최종 확정합니다. 완료 URL의 query 파라미터가 필요하므로 `MyiamAction`의 `onRedirect` 콜백을 사용합니다. ### 흐름 개요 ```text 1. MyiamAction.deregister 오픈 → MyIAM 탈퇴 폼 2. 사용자 확인 → MyIAM이 deregisterRedirectUri 로 ?token= 발송 3. 앱이 onRedirect(uri) 콜백에서 token 추출 4. api.completeDeregistration(token) 호출 → redirect_url, service_user_uid 수신 5. 앱 정리(로컬 데이터 삭제 등) 후 로그아웃 ``` ### 콜백 URL 분리 `MyiamConfig`에 수동 탈퇴 전용 URL을 지정합니다. 미지정 시 `redirectUri`로 fallback되지만, OAuth2 redirect_uri와 혼용되지 않도록 **분리를 권장**합니다. ```dart const MyiamConfig( redirectScheme: 'myapp', redirectHost: 'oauth2callback', // 수동 탈퇴 완료 알림 콜백 (MyIAM이 ?token= 을 여기로 발송) deregisterRedirectScheme: 'myapp', deregisterRedirectHost: 'deregister-callback', ... ) ``` **중요:** `deregisterRedirectUri`는 수동 탈퇴 콜백 수신 전용입니다. OAuth2 authorize/token 요청 어디에도 포함되지 않으며, MyIAM 콘솔에서는 "수동 탈퇴 콜백" 필드에만 등록합니다 (OAuth2 redirect_uri 목록에 추가하면 안 됩니다). ### `onRedirect` 콜백으로 token 처리 탈퇴 완료 시 URL의 query 파라미터(`?token=...`)를 앱까지 가져오려면 `onRedirect` 콜백을 사용합니다. InAppWebView/CustomTab 양쪽에서 동일하게 full Uri를 전달합니다. ```dart MyiamAction.deregister( completeRedirectUrl: config.deregisterRedirectUri, mode: MyiamWebViewMode.customTab, onRedirect: (uri) async { final token = uri.queryParameters['token']; if (token == null || token.isEmpty) return; final api = ref.read(myiamApiProvider); final result = await api.completeDeregistration(token: token); // result.redirectUrl — MyIAM 로그아웃 URL // result.serviceUserUid — 탈퇴된 MyIAM UID await ref.read(authNotifierProvider.notifier).logout(); }, onBack: () => Navigator.of(context).pop(), ) ``` **Supabase 연동 앱이라면** `myiam_supabase_flutter_sdk`의 `SupabaseAuthNotifier.deregister(token)`가 Edge Function을 통해 `completeDeregistration` 호출 + `auth.users` 삭제 + 로그아웃까지 한 번에 처리합니다. ### 플랫폼별 설정 Android — `AndroidManifest.xml`에 deregister-callback host 추가 ```xml ``` **iOS** — `Info.plist`는 scheme 단위로만 등록되므로 추가 작업 불필요. ## Device Authorization 로그인 TV, 데스크톱, CLI 등 WebView를 사용할 수 없는 환경용 RFC 8628 플로우 WebView를 사용할 수 없는 환경(TV, 데스크톱, CLI 등)에서 외부 브라우저를 통해 인증하는 [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) Device Authorization Grant 플로우입니다. ### 흐름 개요 ```mermaid flowchart TD A["DeviceLoginScreen 진입
서버에 device code 요청"] A --> B["사용자 코드 표시
user_code 표시 + 외부 브라우저 자동 오픈"] B --> C["사용자 인증
사용자가 브라우저에서 코드 입력 후 인증 완료"] C --> D["토큰 수신
SDK가 토큰 엔드포인트를 폴링하여 토큰 수신"] D --> E["onLogin 콜백
setAuthenticated 자동 호출"] ``` ### 사용법 ```dart DeviceLoginScreen( onLogin: (tokens) async { final api = ref.read(myiamApiProvider); final userMe = await api.getUser(accessToken: tokens.accessToken); return MyiamUser( uid: userMe.uid, serviceUid: userMe.serviceUid, username: userMe.username, authority: userMe.authority, ); }, onCancel: () => Navigator.of(context).pop(), autoSignup: true, // 선택: 미가입 사용자 자동 가입 ) ``` ### UI 구성 | 상태 | 표시 내용 | | --- | --- | | 대기 중 | user code 표시 (복사 버튼 포함), 카운트다운 타이머, 브라우저 열기 버튼 | | 성공 | 체크 아이콘 + 완료 메시지 | | 만료 | 코드 만료 안내 + 재시도 버튼 | | 에러 | 에러 메시지 + 재시도 버튼 | ### 커스텀 UI (DeviceLoginController) `DeviceLoginController`를 직접 사용하여 커스텀 UI를 구성할 수 있습니다. ```dart final state = ref.watch(deviceLoginControllerProvider); final controller = ref.read(deviceLoginControllerProvider.notifier); // 플로우 시작 await controller.start(autoSignup: true); // 브라우저 수동 열기 controller.openBrowser(); // 취소 controller.cancel(); ``` ## Provider ### [타입] authConfigProvider SDK 설정. ProviderScope에서 오버라이드 필수. ```dart const MyiamConfig( serviceUid: String.fromEnvironment('SERVICE_UID'), oauth2ClientId: String.fromEnvironment('OAUTH2_CLIENT_ID'), apiKey: String.fromEnvironment('API_KEY'), redirectScheme: String.fromEnvironment('REDIRECT_SCHEME'), redirectHost: String.fromEnvironment('REDIRECT_HOST'), // 수동 회원가입을 쓰는 서비스만 설정 signupRedirectScheme: String.fromEnvironment('SIGNUP_REDIRECT_SCHEME'), signupRedirectHost: String.fromEnvironment('SIGNUP_REDIRECT_HOST'), // 수동 탈퇴를 쓰는 서비스만 설정 deregisterRedirectScheme: String.fromEnvironment('DEREGISTER_REDIRECT_SCHEME'), deregisterRedirectHost: String.fromEnvironment('DEREGISTER_REDIRECT_HOST'), ) ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | serviceUid * | String | 서비스 UID | | oauth2ClientId * | String | OAuth2 클라이언트 ID | | apiKey * | String | API 키 | | myiamAppUrl | String | 인증 서버 주소. 기본 `https://app.myiam.io` | | myiamApiUrl | String | REST API 주소. 기본 `https://api.myiam.io` | | redirectScheme | String? | URL 스킴 (예: myiamsample) | | redirectHost | String? | URL 호스트 (예: oauth2callback) | | redirectUri | String? | 커스텀 리다이렉트 URI. 생략 시 {scheme}://{host} | | logoutRedirectUri | String? | 로그아웃 후 리다이렉트 URI. 생략 시 redirectUri와 동일 | | signupRedirectScheme | String? | 수동가입 콜백 URL 스킴. OAuth2 redirect_uri와 별개로 콘솔의 "수동가입 콜백" 필드에 등록 | | signupRedirectHost | String? | 수동가입 콜백 URL 호스트 | | signupRedirectUri | String? | 수동가입 콜백 URI. 미지정 시 {signupScheme}://{signupHost} 또는 redirectUri로 fallback | | deregisterRedirectScheme | String? | 수동 탈퇴 콜백 URL 스킴. OAuth2 redirect_uri와 별개로 콘솔의 "수동 탈퇴 콜백" 필드에 등록 | | deregisterRedirectHost | String? | 수동 탈퇴 콜백 URL 호스트 | | deregisterRedirectUri | String? | 수동 탈퇴 콜백 URI. 미지정 시 {deregisterScheme}://{deregisterHost} 또는 redirectUri로 fallback | | refreshPolicy | TokenRefreshPolicy? | 자동 토큰 갱신 정책. 기본: 만료 시간 80% 시점에 갱신, 지수 백오프. null로 설정 시 비활성화 | ### [타입] authNotifierProvider 인증 상태 관리 Provider. 상태 감시(watch)와 메서드 호출(notifier)을 지원합니다. ```dart // 인증 상태에 따라 화면 전환 home: switch (ref.watch(authNotifierProvider)) { AuthStateLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), AuthStateAuthenticated() => const DashboardScreen(), AuthStateUnauthenticated() => const HomeScreen(), AuthStateError() => const HomeScreen(), } ``` `상태 변경 감지` ```dart // 인증 상태 변경 감지 후 네비게이션 ref.listen(authNotifierProvider, (previous, next) { if (next is AuthStateAuthenticated) { Navigator.of(context).popUntil((route) => route.isFirst); } }); ``` ### [타입] myiamApiProvider REST API 클라이언트 Provider. apiKey 설정이 필요합니다. ```dart final api = ref.read(myiamApiProvider); final user = await api.getUser(accessToken: tokens.accessToken); ``` ### [타입] deviceAuthClientProvider Device Authorization 클라이언트 Provider. device code 요청 및 토큰 폴링을 처리합니다. ```dart final client = ref.read(deviceAuthClientProvider); final deviceAuth = await client.requestDeviceCode(); final tokens = await client.pollForTokens(deviceAuth); ``` ### [타입] deviceLoginControllerProvider Device Login 상태 관리 Provider. DeviceLoginScreen 없이 커스텀 UI를 구성할 때 사용합니다. ```dart final state = ref.watch(deviceLoginControllerProvider); final controller = ref.read(deviceLoginControllerProvider.notifier); // 플로우 시작 await controller.start(autoSignup: true); // 브라우저 수동 열기 controller.openBrowser(); // 취소 controller.cancel(); ``` ### [타입] tokenStorageProvider 토큰·사용자 정보 저장소 Provider. 기본값은 SecureTokenStorage(flutter_secure_storage). TokenStorage를 구현해 오버라이드할 수 있습니다. ```dart ProviderScope( overrides: [ tokenStorageProvider.overrideWithValue(MyCustomTokenStorage()), ], child: const MyApp(), ) ``` ## 타입 ### [타입] MyiamConfig SDK 설정 객체. authConfigProvider를 통해 ProviderScope에서 오버라이드합니다. ```dart class MyiamConfig { final String serviceUid; final String oauth2ClientId; final String apiKey; final String myiamAppUrl; // 기본 https://app.myiam.io final String myiamApiUrl; // 기본 https://api.myiam.io final String? redirectScheme; final String? redirectHost; final String? signupRedirectScheme; final String? signupRedirectHost; final String? deregisterRedirectScheme; final String? deregisterRedirectHost; final TokenRefreshPolicy? refreshPolicy; // 생성자로 직접 넘길 수 있고, 미지정 시 scheme/host로 조립되는 getter String get redirectUri; String get logoutRedirectUri; String get signupRedirectUri; String get deregisterRedirectUri; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | serviceUid * | String | 서비스 UID | | oauth2ClientId * | String | OAuth2 클라이언트 ID | | apiKey * | String | API 키 | | myiamAppUrl | String | 인증 서버 주소. 기본 `https://app.myiam.io` | | myiamApiUrl | String | REST API 주소. 기본 `https://api.myiam.io` | | redirectScheme | String? | URL 스킴 (예: myiamsample) | | redirectHost | String? | URL 호스트 (예: oauth2callback) | | redirectUri | String? | 커스텀 리다이렉트 URI. 생략 시 {scheme}://{host} | | logoutRedirectUri | String? | 로그아웃 후 리다이렉트 URI. 생략 시 redirectUri와 동일 | | signupRedirectScheme | String? | 수동가입 콜백 URL 스킴. OAuth2 redirect_uri와 별개로 콘솔의 "수동가입 콜백" 필드에 등록 | | signupRedirectHost | String? | 수동가입 콜백 URL 호스트 | | signupRedirectUri | String? | 수동가입 콜백 URI. 미지정 시 {signupScheme}://{signupHost} 또는 redirectUri로 fallback | | deregisterRedirectScheme | String? | 수동 탈퇴 콜백 URL 스킴. OAuth2 redirect_uri와 별개로 콘솔의 "수동 탈퇴 콜백" 필드에 등록 | | deregisterRedirectHost | String? | 수동 탈퇴 콜백 URL 호스트 | | deregisterRedirectUri | String? | 수동 탈퇴 콜백 URI. 미지정 시 {deregisterScheme}://{deregisterHost} 또는 redirectUri로 fallback | | refreshPolicy | TokenRefreshPolicy? | 자동 토큰 갱신 정책. 기본: 만료 시간 80% 시점에 갱신, 지수 백오프. null로 설정 시 비활성화 | ### [타입] MyiamTokens OAuth2 토큰 응답 객체. ```dart class MyiamTokens { final String accessToken; final String refreshToken; final String tokenType; final int? expiresIn; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | 액세스 토큰 | | refreshToken * | String | 리프레시 토큰 | | tokenType * | String | 토큰 타입 (기본: bearer) | | expiresIn | int? | 만료 시간 (초) | ### [타입] MyiamUser 인증된 사용자 정보 객체. ```dart class MyiamUser { final String uid; final String serviceUid; final String? username; final List> authority; Map toJson(); } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | uid * | String | 서비스 사용자 UID | | serviceUid * | String | 서비스 UID | | username | String? | 사용자명 | | authority | List\\> | 권한 목록 (기본: []) | | toJson() | Map\ | JSON 직렬화 | ### [타입] TokenStorage 토큰·사용자 정보 저장 인터페이스. 커스텀 저장소를 만들 때 구현합니다. ```dart abstract class TokenStorage { Future read(); Future write(MyiamTokens tokens); Future delete(); Future readUser(); Future writeUser(MyiamUser user); Future deleteUser(); } ``` ### [타입] AuthState 인증 상태. sealed class로 패턴 매칭이 가능합니다. ```dart sealed class AuthState {} class AuthStateLoading extends AuthState {} class AuthStateUnauthenticated extends AuthState {} class AuthStateAuthenticated extends AuthState { final MyiamTokens tokens; final MyiamUser user; } class AuthStateError extends AuthState { final String message; } ``` #### 상태 | 속성명 | 설명 | | --- | --- | | AuthStateLoading | 초기 로딩 | | AuthStateUnauthenticated | 비인증 상태 | | AuthStateAuthenticated | 인증됨 (tokens, user 포함) | | AuthStateError | 에러 (message 포함) | `사용 예시` ```dart // 인증 상태에 따라 화면 전환 home: switch (ref.watch(authNotifierProvider)) { AuthStateLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), AuthStateAuthenticated() => const DashboardScreen(), AuthStateUnauthenticated() => const HomeScreen(), AuthStateError() => const HomeScreen(), } ``` ### [타입] TokenRefreshPolicy 자동 토큰 갱신 정책. MyiamConfig.refreshPolicy로 설정합니다. ```dart class TokenRefreshPolicy { final double thresholdFactor; // 기본: 0.8 (만료 시간의 80%) final int minIntervalSeconds; // 기본: 30 (최소 갱신 간격) } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | thresholdFactor | double | 만료 시간 대비 갱신 시점 비율 (기본: 0.8 = 80%) | | minIntervalSeconds | int | 최소 갱신 간격 (초, 기본: 30). thresholdFactor 계산 결과가 이 값보다 작으면 이 값을 사용 | ### [타입] TokenRefreshRecord 토큰 갱신 이력 레코드. AuthNotifier.refreshHistory에서 조회합니다. ```dart class TokenRefreshRecord { final DateTime timestamp; final TokenRefreshTrigger trigger; // auto, resume, manual, retry final bool success; final int? expiresIn; final String? accessTokenPrefix; final String? refreshTokenPrefix; final String? error; final String? errorType; final int? httpStatus; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | timestamp * | DateTime | 갱신 시도 시각 | | trigger * | TokenRefreshTrigger | 갱신 트리거 (auto, resume, manual, retry) | | success * | bool | 갱신 성공 여부 | | expiresIn | int? | 새 토큰 만료 시간 (초, 성공 시) | | error | String? | 실패 시 에러 메시지 | | errorType | String? | 에러 타입명 (실패 시) | | httpStatus | int? | HTTP 상태 코드 (API 에러 시) | ### [타입] DeviceAuthResponse Device Authorization 요청 응답. device code, user code, 인증 URL 등을 포함합니다. ```dart class DeviceAuthResponse { final String deviceCode; final String userCode; final String verificationUri; final String? verificationUriComplete; final int expiresIn; final int interval; String buildVerificationUrl(MyiamConfig config, {bool? autoSignup}); } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | deviceCode * | String | 서버 폴링용 코드 | | userCode * | String | 사용자에게 표시할 코드 | | verificationUri * | String | 사용자가 접속할 인증 URL | | verificationUriComplete | String? | user_code가 포함된 인증 URL | | expiresIn * | int | device code 만료 시간 (초) | | interval * | int | 폴링 간격 (초, 기본 5) | | buildVerificationUrl() | String | client_id, service_uid, auto_signup을 포함한 인증 URL 생성 | ### [타입] DeviceLoginState Device Login 상태. sealed class로 패턴 매칭이 가능합니다. ```dart sealed class DeviceLoginState {} class DeviceLoginLoading extends DeviceLoginState {} class DeviceLoginWaiting extends DeviceLoginState { final String userCode; final String verificationUrl; final int remainingSeconds; final int expiresIn; } class DeviceLoginSuccess extends DeviceLoginState { final MyiamTokens tokens; } class DeviceLoginExpired extends DeviceLoginState {} class DeviceLoginError extends DeviceLoginState { final String message; } ``` #### 상태 | 속성명 | 설명 | | --- | --- | | DeviceLoginLoading | device code 요청 중 | | DeviceLoginWaiting | 사용자 인증 대기 중 (userCode, verificationUrl, remainingSeconds, expiresIn) | | DeviceLoginSuccess | 인증 완료 (tokens) | | DeviceLoginExpired | device code 만료 | | DeviceLoginError | 에러 발생 (message) | ## 스크린 ### [함수] LoginScreen LoginWebView를 감싼 스크린. 토큰 수신 → onLogin으로 사용자 조회 → setAuthenticated 자동 호출. `시그니처` ```dart LoginScreen({required onLogin, onBack, mode, autoSignup}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | onLogin * | Future\ Function(MyiamTokens) | 토큰으로 사용자 정보를 조회하는 콜백 | | onBack | VoidCallback? | 취소 시 콜백 | | mode | MyiamWebViewMode | 기본: customTab | | autoSignup | bool? | OAuth2 URL에 auto_signup 전달. true면 미가입 사용자 자동 가입, null이면 서버 설정을 따름 | `예시` ```dart LoginScreen( onLogin: (tokens) async { final api = ref.read(myiamApiProvider); final userMe = await api.getUser(accessToken: tokens.accessToken); return MyiamUser( uid: userMe.uid, serviceUid: userMe.serviceUid, username: userMe.username, authority: userMe.authority, ); }, onBack: () => Navigator.of(context).pop(), ) ``` ### [함수] SignupScreen SignupWebView를 감싼 스크린. LoginScreen과 동일한 구조. `시그니처` ```dart SignupScreen({required onSignup, onBack, mode, autoSignup}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | onSignup * | Future\ Function(MyiamTokens) | 토큰으로 사용자 정보를 조회하는 콜백 | | onBack | VoidCallback? | 취소 시 콜백 | | mode | MyiamWebViewMode | 기본: inAppWebView | | autoSignup | bool? | OAuth2 URL에 auto_signup 전달. true면 미가입 사용자 자동 가입, null이면 서버 설정을 따름 | `예시` ```dart SignupScreen( onSignup: (tokens) async { final api = ref.read(myiamApiProvider); final userMe = await api.getUser(accessToken: tokens.accessToken); return MyiamUser( uid: userMe.uid, serviceUid: userMe.serviceUid, username: userMe.username, authority: userMe.authority, ); }, onBack: () => Navigator.of(context).pop(), ) ``` ### [함수] LogoutScreen LogoutWebView를 감싼 스크린. 서버 로그아웃 후 로컬 세션을 자동 정리합니다. `시그니처` ```dart LogoutScreen({mode}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | mode | MyiamWebViewMode | 기본: inAppWebView | `예시` ```dart Navigator.of(context).push( MaterialPageRoute(builder: (_) => const LogoutScreen()), ); ``` ### [함수] DeviceLoginScreen Device Authorization Grant(RFC 8628) 스크린. WebView를 사용할 수 없는 환경(TV, 데스크톱, CLI 등)에서 외부 브라우저를 통해 인증합니다. 토큰 수신 → onLogin → setAuthenticated 자동 호출. `시그니처` ```dart DeviceLoginScreen({required onLogin, onCancel, autoSignup}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | onLogin * | Future\ Function(MyiamTokens) | 토큰으로 사용자 정보를 조회하는 콜백 | | onCancel | VoidCallback? | 취소 시 콜백. 미지정 시 Navigator.pop() | | autoSignup | bool? | 인증 URL에 auto_signup 전달. null이면 서버 기본값 사용 | `예시` ```dart DeviceLoginScreen( onLogin: (tokens) async { final api = ref.read(myiamApiProvider); final userMe = await api.getUser(accessToken: tokens.accessToken); return MyiamUser( uid: userMe.uid, serviceUid: userMe.serviceUid, username: userMe.username, authority: userMe.authority, ); }, onCancel: () => Navigator.of(context).pop(), autoSignup: true, // 선택: 미가입 사용자 자동 가입 ) ``` ## 위젯 ### [함수] LoginWebView OAuth2 로그인 플로우를 처리하는 저수준 위젯. `시그니처` ```dart LoginWebView({required onSuccess, onError, onCancelled, mode, autoSignup}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | onSuccess * | void Function(MyiamTokens) | 토큰 수신 시 콜백 | | onError | void Function(String)? | 에러 시 콜백 | | onCancelled | VoidCallback? | 브라우저 닫힘 시 콜백 (customTab만) | | mode | MyiamWebViewMode | 기본: customTab | | autoSignup | bool? | OAuth2 URL에 auto_signup 전달. null이면 서버 기본값 사용 | `예시` ```dart LoginWebView( onSuccess: (tokens) { // 토큰 수신 후 처리 }, onError: (message) { // 에러 처리 }, onCancelled: () { // customTab 모드에서 브라우저 닫힘 시 }, ) ``` ### [함수] SignupWebView OAuth2 회원가입 플로우를 처리하는 저수준 위젯. LoginWebView와 동일한 인터페이스. 수동 회원가입의 1차 콜백(?token=)을 감지하면 api.completeRegistration을 자동 호출한 뒤 응답의 redirect_url로 재진입해 2차 콜백(?code=&state=)까지 한 흐름으로 처리합니다. `시그니처` ```dart SignupWebView({required onSuccess, onError, onCancelled, mode, autoSignup}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | onSuccess * | void Function(MyiamTokens) | 토큰 수신 시 콜백 | | onError | void Function(String)? | 에러 시 콜백 | | onCancelled | VoidCallback? | 브라우저 닫힘 시 콜백 (customTab만) | | mode | MyiamWebViewMode | 기본: inAppWebView | | autoSignup | bool? | OAuth2 URL에 auto_signup 전달. null이면 서버 기본값 사용 | ### [함수] LogoutWebView 서버 로그아웃 후 로컬 세션을 정리하는 저수준 위젯. `시그니처` ```dart LogoutWebView({required onLoggedOut, mode}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | onLoggedOut * | VoidCallback | 로그아웃 완료 시 콜백 | | mode | MyiamWebViewMode | 기본: inAppWebView | ### [함수] AuthGuard 인증이 필요한 위젯을 감싸는 보호 위젯. `시그니처` ```dart AuthGuard({required child, required onUnauthenticated, loadingWidget}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | child * | Widget | 인증 시 표시할 위젯 | | onUnauthenticated * | VoidCallback | 비인증 시 콜백 | | loadingWidget | Widget | `AuthStateLoading` 중에 표시할 위젯. 기본: 중앙 정렬 `CircularProgressIndicator` | `예시` ```dart AuthGuard( onUnauthenticated: () => Navigator.of(context).pushReplacementNamed('/login'), child: const DashboardPage(), ) ``` ### [함수] MyiamAction 사용자 액션을 처리하는 위젯. API 호출 + WebView 표시를 내부에서 자동 처리합니다. Named Constructor로 액션을 선택합니다: editProfile, editEmail, setPassword, resetPassword, deregister, passkey. `시그니처` ```dart MyiamAction.editProfile({required completeRedirectUrl, ...}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | completeRedirectUrl * | String | 완료 후 리다이렉트 URL | | onComplete | VoidCallback? | 완료 시 콜백. onRedirect와 병행 호출됨 | | onRedirect | ValueChanged\? | 리다이렉트 URL의 full Uri 전달. ?token=... 같은 query 파라미터가 필요한 플로우(수동 탈퇴 등)에서 사용. InAppWebView/CustomTab 양쪽에서 동일하게 발동 | | onBack | VoidCallback? | 뒤로 가기 시 콜백 | | onError | void Function(String)? | 에러 시 콜백 | | state | String? | 콜백 URL에 그대로 실려 돌아오는 임의 문자열 | | editFields | List\? | editProfile에서 수정 가능 필드 제한 | | loadingWidget | Widget | 액션 URL을 받아오는 동안 표시할 위젯. 기본: 중앙 정렬 `CircularProgressIndicator` | | mode | MyiamWebViewMode | 기본: inAppWebView. passkey 등 브라우저 인증이 필요하면 customTab 사용 | `예시` ```dart final config = ref.read(authConfigProvider); // 프로필 수정 MyiamAction.editProfile( completeRedirectUrl: config.redirectUri, editFields: ['nickname', 'mobile_number'], onComplete: () => Navigator.of(context).pop(), onBack: () => Navigator.of(context).pop(), ) // 비밀번호 재설정 MyiamAction.resetPassword( completeRedirectUrl: config.redirectUri, onComplete: () => Navigator.of(context).pop(), onBack: () => Navigator.of(context).pop(), ) // 회원 탈퇴 MyiamAction.deregister( completeRedirectUrl: config.redirectUri, onComplete: () => Navigator.of(context).pop(), onBack: () => Navigator.of(context).pop(), ) // passkey 관리 — Custom Tab 모드 MyiamAction.passkey( completeRedirectUrl: config.redirectUri, onComplete: () => Navigator.of(context).pop(), onBack: () => Navigator.of(context).pop(), mode: MyiamWebViewMode.customTab, ) ``` ### [함수] MyiamAction.launch() 화면 전환 없이 현재 화면 위에 Custom Tab을 바로 여는 static 메서드. 위젯으로 배치하지 않고 버튼 콜백 등에서 직접 호출합니다. `시그니처` ```dart static Future MyiamAction.launch(ref, {required action}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | ref * | WidgetRef | Riverpod WidgetRef | | action * | MyiamAction | 실행할 액션 위젯 인스턴스 | `예시` ```dart await MyiamAction.launch( ref, action: MyiamAction.editProfile( completeRedirectUrl: config.redirectUri, editFields: ['nickname', 'mobile_number'], onComplete: () => Navigator.of(context).pop(), onError: (e) => debugPrint(e), ), ); ``` ### [함수] MyiamWebPage 임의의 URL을 InAppWebView로 표시하는 범용 위젯. `시그니처` ```dart MyiamWebPage({required url, redirectUri, onRedirect, onComplete, onBack, transparentBackground}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | url * | String | 표시할 URL | | redirectUri | String? | 리다이렉트 매칭용 prefix. 매칭되면 onRedirect + onComplete 호출 | | onRedirect | ValueChanged\? | 리다이렉트 URL의 full Uri 전달. query 파라미터가 필요한 경우 사용 | | onComplete | VoidCallback? | 완료 시 콜백. onRedirect와 병행 호출됨 | | onBack | VoidCallback? | history.back() 시 히스토리가 없으면 호출 | | transparentBackground | bool | 배경 투명 설정. 기본: false | `예시` ```dart MyiamWebPage( url: 'https://example.com/page', redirectUri: 'myiamsample://complete', onComplete: () => Navigator.of(context).pop(), onBack: () => Navigator.of(context).pop(), ) ``` ## AuthNotifier 메서드 ### [함수] setAuthenticated 인증 상태로 전환합니다. 토큰과 사용자 정보를 SecureStorage에 저장합니다. LoginScreen/SignupScreen에서 자동 호출됩니다. `시그니처` ```dart authNotifierProvider.notifier.setAuthenticated(tokens, user) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | tokens * | MyiamTokens | OAuth2 토큰 | | user * | MyiamUser | 사용자 정보 | **반환값:** `Future` `예시` ```dart // LoginScreen/SignupScreen 내부에서 자동 호출됨 // 직접 사용하는 경우: ref.read(authNotifierProvider.notifier).setAuthenticated(tokens, user); ``` ### [함수] refreshToken 토큰을 갱신합니다. 서버 거부(MyiamApiError) 시 자동 로그아웃되며, 네트워크 오류 시에는 세션을 유지합니다. `시그니처` ```dart authNotifierProvider.notifier.refreshToken() ``` **반환값:** `Future` — 새로 발급된 토큰 `예시` ```dart try { await ref.read(authNotifierProvider.notifier).refreshToken(); } catch (e) { // 갱신 실패 시 자동 로그아웃됨 } ``` ### [함수] refreshHistory 토큰 갱신 이력을 조회합니다. 자동/수동 갱신 모두 기록됩니다. `시그니처` ```dart authNotifierProvider.notifier.refreshHistory ``` **반환값:** `List` — 갱신 이력 목록 `예시` ```dart // 토큰 갱신 이력 조회 final history = ref.read(authNotifierProvider.notifier).refreshHistory; // 자동 갱신 스케줄 재설정 ref.read(authNotifierProvider.notifier).rescheduleAutoRefresh(); // 다음 자동 갱신 예정 시간 final nextRefresh = ref.read(authNotifierProvider.notifier).nextRefreshAt; ``` ### [함수] logout 서버에 토큰 무효화 요청(deleteToken)을 보낸 뒤 로컬 토큰과 사용자 정보를 삭제합니다. 서버 요청이 실패하더라도 로컬 정리는 항상 진행됩니다. `시그니처` ```dart authNotifierProvider.notifier.logout() ``` **반환값:** `Future` `예시` ```dart // 서버 로그아웃 (WebView) LogoutScreen() // 또는 프로그래밍 방식 로그아웃 await ref.read(authNotifierProvider.notifier).logout(); ``` ## 에러 ### [타입] MyiamCallbackError OAuth2 콜백 처리 중 발생하는 에러. ```dart class MyiamCallbackError { final MyiamCallbackErrorCode code; // missingCode, missingPkceState, stateMismatch } ``` #### 에러 코드 | 속성명 | 설명 | | --- | --- | | missingCode | 콜백 URL에 code가 없음 | | missingPkceState | PKCE state를 찾을 수 없음 | | stateMismatch | state 불일치 (CSRF 의심) | ### [타입] MyiamApiError API 호출 실패 시 발생하는 에러. ```dart class MyiamApiError { final int status; final int code; final String message; factory MyiamApiError.fromResponse(Response response); } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | status * | int | HTTP 상태 코드 | | code * | int | MyIAM 에러 코드 | | message * | String | 에러 메시지 | | fromResponse() | factory | HTTP Response에서 MyiamApiError 생성 | `에러 처리 예시` ```dart try { final user = await api.getUser(accessToken: accessToken); } on MyiamApiError catch (e) { print('${e.status} ${e.code} ${e.message}'); } ``` ## 디버그 로깅 ### [타입] MyiamLogger SDK 내부 디버그 로그를 제어합니다. 기본 비활성. ```dart // 앱 시작 시 활성화 MyiamLogger.enabled = true; // 릴리스 빌드에서는 비활성화 MyiamLogger.enabled = false; ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | enabled | static bool | true로 설정하면 SDK 내부 로그 출력 (기본: false) | | log(tag, message) | static void | enabled가 true일 때만 debugPrint('$tag $message') 호출 | OAuthWebView의 콜백 흐름(deep link 수신, 코드 교환, 가입 토큰 처리, Custom Tab 열기/닫기)과 AuthNotifier의 토큰 갱신 로그가 출력됩니다. 민감 데이터(auth code, token 값)는 로그에 포함되지 않습니다. **주의:** 릴리스 빌드에서는 반드시 비활성화하세요. --- ## Flutter SDK REST API 레퍼런스 URL: https://myiam.io/docs/sdk/flutter/rest-api myiam_flutter_sdk - Flutter 앱을 위한 REST API SDK 문서. 패키지: https://pub.dev/packages/myiam_flutter_sdk 릴리스 노트: https://myiam.io/docs/release-notes/flutter-sdk # Flutter SDK REST API 레퍼런스 `MyiamApi`를 통해 MyIAM REST API를 호출합니다. 토큰 관리, 사용자 조회, 회원 가입/탈퇴 완료 등을 지원합니다. 사용자 액션(프로필 수정, 비밀번호 변경 등)은 [MyiamAction](https://myiam.io/docs/sdk/flutter/auth#myiamaction) 위젯을 사용하세요. ## 시작 가이드 기본 사용법 ### 기본 사용법 `myiamApiProvider`로 API 클라이언트를 가져오고, 인증 상태에서 토큰을 꺼내 사용합니다. `apiKey` 설정이 필요합니다. ```dart final api = ref.read(myiamApiProvider); // 인증 상태에서 토큰과 사용자 정보 가져오기 final authState = ref.read(authNotifierProvider); if (authState is AuthStateAuthenticated) { final accessToken = authState.tokens.accessToken; final serviceUserUid = authState.user.uid; } ``` ## 토큰 API ### [함수] getTokenInfo Access Token의 유효성을 조회합니다. `시그니처` ```dart api.getTokenInfo(accessToken) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | 조회할 액세스 토큰 | **반환값:** `Future` — active, expiredAt, serviceUserUid 등을 포함한 토큰 정보 `예시` ```dart final info = await api.getTokenInfo(accessToken); // info.active, info.expiredAt, info.serviceUserUid ``` ### [함수] deleteToken Access Token을 만료 처리합니다. target 값으로 삭제 범위를 지정할 수 있습니다. `시그니처` ```dart api.deleteToken({required accessToken, serviceUserUid, target, refreshTokenDelete}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | 삭제할 액세스 토큰 | | serviceUserUid | String? | 대상 서비스 사용자 UID (target 지정 시 필요) | | target | String? | 삭제 범위: 미지정(현재 토큰만), ACCOUNT_USER(해당 사용자 전체), SERVICE(서비스 전체) | | refreshTokenDelete | bool? | 리프레시 토큰도 함께 삭제할지 여부 | **반환값:** `Future` `예시` ```dart // 현재 토큰만 삭제 await api.deleteToken(accessToken: accessToken); // 해당 사용자의 모든 토큰 삭제 await api.deleteToken( accessToken: accessToken, serviceUserUid: serviceUserUid, target: 'ACCOUNT_USER', ); ``` ## 사용자 API ### [함수] getUser 현재 인증된 사용자 정보를 조회합니다. `시그니처` ```dart api.getUser({required accessToken, serviceUserUid}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | 액세스 토큰 | | serviceUserUid | String? | 조회할 서비스 사용자 UID. 생략 시 액세스 토큰의 사용자 | **반환값:** `Future` — uid, serviceUid, username, authority를 포함한 사용자 정보 `예시` ```dart final user = await api.getUser(accessToken: accessToken); // user.uid, user.serviceUid, user.username, user.authority ``` ## 서비스 사용자 API ### [함수] getServiceUser 서비스 사용자의 기본 정보를 조회합니다. `시그니처` ```dart api.getServiceUser({required accessToken, required serviceUserUid}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | 액세스 토큰 | | serviceUserUid * | String | 서비스 사용자 UID | **반환값:** `Future` — uid, status, createdAt 등을 포함한 서비스 사용자 정보 `예시` ```dart final serviceUser = await api.getServiceUser( accessToken: accessToken, serviceUserUid: serviceUserUid, ); // serviceUser.uid, serviceUser.status, serviceUser.createdAt ``` ### [함수] getServiceUserProfile 서비스 사용자의 프로필 정보를 조회합니다. `시그니처` ```dart api.getServiceUserProfile({required accessToken, required serviceUserUid}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | 액세스 토큰 | | serviceUserUid * | String | 서비스 사용자 UID | **반환값:** `Future` — email, name, address 등을 포함한 프로필 정보 `예시` ```dart final profile = await api.getServiceUserProfile( accessToken: accessToken, serviceUserUid: serviceUserUid, ); // profile.data.email, profile.data.name, profile.data.address?.zipcode ``` ## 회원 API ### [함수] completePreparation 회원 가입 준비를 완료하고 다음 단계 리다이렉트 URL을 반환합니다. `시그니처` ```dart api.completePreparation({required token, profileData}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | String | 가입 준비 완료 토큰 | | profileData | UserProfiles? | 사용자 프로필 데이터 (name, email, customFields 등) | **반환값:** `Future` — redirectUrl을 포함한 결과 `예시` ```dart // 토큰만 전달 final result = await api.completePreparation(token: token); // result.redirectUrl // 프로필 데이터와 함께 전달 final result = await api.completePreparation( token: token, profileData: UserProfiles( name: '홍길동', email: 'user@example.com', customFields: {'company': 'MyIAM'}, ), ); ``` ### [함수] completeRegistration 수동 회원 가입을 완료합니다. SignupWebView가 수동가입 콜백(?token=)을 수신하면 이 메서드를 자동으로 호출합니다. 직접 호출은 customFields를 전달하거나 커스텀 가입 UI를 구현할 때만 필요합니다. `시그니처` ```dart api.completeRegistration({required token, customFields}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | String | 가입 완료 토큰 | | customFields | Map\? | 사용자 정의 필드 | **반환값:** `Future` — redirectUrl, user 정보를 포함한 결과 `예시` ```dart final result = await api.completeRegistration(token: token); // result.redirectUrl, result.user.uid, result.user.profiles.email // custom_fields와 함께 전달 final result = await api.completeRegistration( token: token, customFields: {'company': 'MyIAM', 'role': 'admin'}, ); ``` ### [함수] completeDeregistration 수동 회원 탈퇴를 완료합니다. `시그니처` ```dart api.completeDeregistration({required token, type, reason, message}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | token * | String | 탈퇴 완료 토큰 | | type | String? | 탈퇴 사유 유형 | | reason | String? | 탈퇴 사유 상세 | | message | String? | 탈퇴 메시지 | **반환값:** `Future` — redirectUrl, serviceUserUid를 포함한 결과 `예시` ```dart final result = await api.completeDeregistration( token: token, type: 'NOT_USEFUL', reason: '서비스를 더 이상 이용하지 않습니다.', ); // result.redirectUrl, result.serviceUserUid ``` ## 에러 처리 API 실패 시 `MyiamApiError`가 throw됩니다. ### [타입] MyiamApiError API 호출 실패 시 발생하는 에러. ```dart class MyiamApiError { final int status; final int code; final String message; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | status * | int | HTTP 상태 코드 | | code * | int | MyIAM 에러 코드 | | message * | String | 에러 메시지 | `에러 처리 예시` ```dart try { final user = await api.getUser(accessToken: accessToken); } on MyiamApiError catch (e) { print('${e.status} ${e.code} ${e.message}'); } ``` --- ## Firebase Flutter SDK API 레퍼런스 URL: https://myiam.io/docs/sdk/flutter/firebase myiam_firebase_flutter_sdk — MyIAM 인증과 Firebase 서비스를 연결하는 통합 SDK 문서. 패키지: https://pub.dev/packages/myiam_firebase_flutter_sdk 릴리스 노트: https://myiam.io/docs/release-notes/firebase-flutter-sdk # Firebase Flutter SDK API 레퍼런스 MyIAM 인증과 Firebase 서비스를 연결하는 통합 SDK. MyIAM 로그인 시 자동으로 Firebase Custom Token 브릿지를 연결합니다. ## 시작 가이드 설치, 빠른 시작, 인증 플로우 ### 설치 ```bash flutter pub add myiam_firebase_flutter_sdk # 필요한 Firebase 패키지도 함께 설치 flutter pub add firebase_core firebase_auth cloud_functions ``` 이 SDK는 `myiam_flutter_sdk` 위에 Firebase 연동 레이어를 추가합니다. 먼저 [Flutter Quickstart](https://myiam.io/docs/quickstart/flutter)를 완료하세요. ### 빠른 시작 ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; import 'package:myiam_firebase_flutter_sdk/myiam_firebase_flutter_sdk.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); runApp( ProviderScope( overrides: [ authConfigProvider.overrideWithValue( const MyiamConfig( serviceUid: String.fromEnvironment('SERVICE_UID'), oauth2ClientId: String.fromEnvironment('OAUTH2_CLIENT_ID'), apiKey: String.fromEnvironment('API_KEY'), redirectScheme: String.fromEnvironment('REDIRECT_SCHEME'), redirectHost: String.fromEnvironment('REDIRECT_HOST'), ), ), ], child: const MyApp(), ), ); } // firebaseAuthProvider로 통합 인증 상태 관리 final authState = ref.watch(firebaseAuthProvider); // 토큰 갱신 (MyIAM + Firebase) await ref.read(firebaseAuthProvider.notifier).refreshToken(); // 로그아웃 (MyIAM + Firebase) await ref.read(firebaseAuthProvider.notifier).logout(); ``` ### 인증 플로우 ```mermaid flowchart TD A["MyIAM 로그인
myiam_flutter_sdk의 LoginScreen/SignupScreen으로 OAuth2 인증"] A --> B["firebaseAuthProvider가 인증 감지
authNotifierProvider의 상태 변경을 자동으로 감시"] B --> C["Cloud Function 호출
mintFirebaseToken에 access_token을 전송하여 Custom Token 획득"] C --> D["Firebase Auth 로그인
signInWithCustomToken()으로 Firebase 서비스 접근 가능"] D --> E["로그아웃
Firebase signOut() + MyIAM logout() 양쪽 모두 세션 정리"] ``` ## 커스터마이징 Cloud Function 이름 변경, 멀티 Firebase 프로젝트 ### Cloud Function 이름 변경 기본 Cloud Function 이름은 `mintFirebaseToken`입니다. 다른 이름을 사용하려면 `firebaseAuthBridgeProvider`를 override하세요. `Cloud Function 이름 변경` ```dart ProviderScope( overrides: [ authConfigProvider.overrideWithValue(/* ... */), firebaseAuthBridgeProvider.overrideWithValue( FirebaseAuthBridge(functionName: 'myCustomFunction'), ), ], child: const MyApp(), ) ``` ### Firebase 인스턴스 커스터마이징 멀티 프로젝트 환경 등에서 별도의 Firebase 인스턴스를 사용할 수 있습니다. `Firebase 인스턴스 커스터마이징` ```dart firebaseAuthBridgeProvider.overrideWithValue( FirebaseAuthBridge( firebaseAuth: FirebaseAuth.instanceFor(app: secondaryApp), functions: FirebaseFunctions.instanceFor(app: secondaryApp), ), ) ``` ## Provider ### [타입] firebaseAuthProvider MyIAM + Firebase 통합 인증 상태 Provider. authNotifierProvider를 감시하여 MyIAM 인증 성공 시 자동으로 Firebase Custom Token 브릿지를 연결합니다. ```dart // 인증 상태에 따라 화면 전환 home: switch (ref.watch(firebaseAuthProvider)) { FirebaseAuthLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), FirebaseAuthAuthenticated() => const DashboardScreen(), FirebaseAuthUnauthenticated() => const HomeScreen(), FirebaseAuthError() => const HomeScreen(), } ``` `Firebase 연결 상태 확인` ```dart // Firebase 연결 상태 확인 final authState = ref.watch(firebaseAuthProvider); if (authState is FirebaseAuthAuthenticated) { if (authState.firebaseConnected) { // Firebase 서비스 사용 가능 } else if (authState.firebaseError != null) { // Firebase 연결 실패: authState.firebaseError } else { // Firebase 연결 중... } } ``` ### [타입] firebaseAuthBridgeProvider Firebase Custom Token 브릿지 서비스 Provider. Cloud Function 이름이나 Firebase 인스턴스를 변경하려면 override하세요. ```dart final bridge = ref.read(firebaseAuthBridgeProvider); // 현재 Firebase 사용자 final user = bridge.currentUser; // Firebase 인증 상태 스트림 bridge.authStateChanges.listen((user) { // ... }); ``` ## 타입 ### [타입] FirebaseAuthState MyIAM + Firebase 통합 인증 상태. sealed class로 패턴 매칭이 가능합니다. ```dart sealed class FirebaseAuthState {} class FirebaseAuthLoading extends FirebaseAuthState {} class FirebaseAuthUnauthenticated extends FirebaseAuthState {} class FirebaseAuthAuthenticated extends FirebaseAuthState { final MyiamTokens myiamTokens; final MyiamUser myiamUser; final bool firebaseConnected; final String? firebaseError; } class FirebaseAuthError extends FirebaseAuthState { final String message; } ``` #### 상태 | 속성명 | 설명 | | --- | --- | | FirebaseAuthLoading | 초기 로딩 | | FirebaseAuthUnauthenticated | 비인증 상태 | | FirebaseAuthAuthenticated | 인증됨 (MyIAM 토큰/사용자 + Firebase 연결 상태 포함) | | FirebaseAuthError | 에러 (message 포함) | `사용 예시` ```dart // 인증 상태에 따라 화면 전환 home: switch (ref.watch(firebaseAuthProvider)) { FirebaseAuthLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), FirebaseAuthAuthenticated() => const DashboardScreen(), FirebaseAuthUnauthenticated() => const HomeScreen(), FirebaseAuthError() => const HomeScreen(), } ``` ### [타입] FirebaseAuthAuthenticated 인증 완료 상태. MyIAM 사용자 정보와 Firebase 연결 상태를 포함합니다. ```dart class FirebaseAuthAuthenticated extends FirebaseAuthState { final MyiamTokens myiamTokens; final MyiamUser myiamUser; final bool firebaseConnected; final String? firebaseError; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | myiamTokens * | MyiamTokens | MyIAM access/refresh 토큰 | | myiamUser * | MyiamUser | MyIAM 사용자 정보 | | firebaseConnected | bool | Firebase 인증 완료 여부 (기본: false) | | firebaseError | String? | Firebase 연결 에러 메시지 | ### [타입] FirebaseAuthBridge MyIAM access_token을 Firebase Custom Token으로 교환하고 Firebase Auth에 로그인하는 브릿지 서비스. ```dart class FirebaseAuthBridge { FirebaseAuthBridge({ FirebaseAuth? firebaseAuth, FirebaseFunctions? functions, String functionName = 'mintFirebaseToken', }); } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | firebaseAuth | FirebaseAuth? | Firebase Auth 인스턴스. 생략 시 기본 인스턴스 사용 | | functions | FirebaseFunctions? | Cloud Functions 인스턴스. 생략 시 기본 인스턴스 사용 | | functionName | String | Cloud Function 이름 (기본: mintFirebaseToken) | ## FirebaseAuthNotifier 메서드 ### [함수] refreshToken MyIAM 토큰을 갱신하고 Firebase에 재연결합니다. MyIAM 토큰 갱신 실패 시 자동으로 로그아웃됩니다. `시그니처` ```dart firebaseAuthProvider.notifier.refreshToken() ``` **반환값:** `Future` `예시` ```dart // MyIAM 토큰 갱신 + Firebase 재연결 await ref.read(firebaseAuthProvider.notifier).refreshToken(); ``` ### [함수] logout Firebase signOut()을 먼저 실행한 후 MyIAM logout()을 호출합니다. Firebase signOut 실패와 무관하게 MyIAM logout은 항상 실행됩니다. `시그니처` ```dart firebaseAuthProvider.notifier.logout() ``` **반환값:** `Future` `예시` ```dart // MyIAM + Firebase 양쪽 모두 로그아웃 await ref.read(firebaseAuthProvider.notifier).logout(); ``` ## FirebaseAuthBridge 메서드 ### [함수] signInWithMyiamToken MyIAM access_token으로 Cloud Function을 호출하여 Firebase Custom Token을 받고 Firebase Auth에 로그인합니다. 일반적으로 직접 호출할 필요 없이 firebaseAuthProvider가 자동으로 호출합니다. `시그니처` ```dart bridge.signInWithMyiamToken(accessToken) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | MyIAM access_token | **반환값:** `Future` — Firebase 인증 결과 ### [함수] signOut Firebase에서 로그아웃합니다. `시그니처` ```dart bridge.signOut() ``` **반환값:** `Future` ### [함수] currentUser 현재 Firebase 인증 사용자를 반환합니다. `시그니처` ```dart bridge.currentUser ``` **반환값:** `User?` — Firebase 인증 사용자 또는 null ### [함수] authStateChanges Firebase 인증 상태 변경 스트림. `시그니처` ```dart bridge.authStateChanges ``` **반환값:** `Stream` — 인증 상태 변경 시 사용자 또는 null을 방출 --- ## Supabase Flutter SDK API 레퍼런스 URL: https://myiam.io/docs/sdk/flutter/supabase myiam_supabase_flutter_sdk — MyIAM 인증과 Supabase 서비스를 연결하는 통합 SDK 문서. 패키지: https://pub.dev/packages/myiam_supabase_flutter_sdk 릴리스 노트: https://myiam.io/docs/release-notes/supabase-flutter-sdk # Supabase Flutter SDK API 레퍼런스 MyIAM 인증과 Supabase 서비스를 연결하는 통합 SDK. MyIAM 로그인 시 자동으로 Supabase Edge Function을 통해 세션 브릿지를 연결하며, Supabase 세션 만료 시 자동 재연결을 지원합니다. ## 시작 가이드 설치, 빠른 시작, 인증 플로우 ### 설치 ```bash flutter pub add myiam_supabase_flutter_sdk # 필요한 Supabase 패키지도 함께 설치 flutter pub add supabase_flutter ``` 이 SDK는 `myiam_flutter_sdk` `>=0.8.0` 위에 Supabase 연동 레이어를 추가합니다. 먼저 [Flutter Quickstart](https://myiam.io/docs/quickstart/flutter)를 완료하세요. ### 빠른 시작 ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:myiam_flutter_sdk/myiam_flutter_sdk.dart'; import 'package:myiam_supabase_flutter_sdk/myiam_supabase_flutter_sdk.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: const String.fromEnvironment('SUPABASE_URL'), anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'), ); runApp( ProviderScope( overrides: [ authConfigProvider.overrideWithValue( const MyiamConfig( serviceUid: String.fromEnvironment('SERVICE_UID'), oauth2ClientId: String.fromEnvironment('OAUTH2_CLIENT_ID'), apiKey: String.fromEnvironment('API_KEY'), redirectScheme: String.fromEnvironment('REDIRECT_SCHEME'), redirectHost: String.fromEnvironment('REDIRECT_HOST'), ), ), ], child: const MyApp(), ), ); } // supabaseAuthProvider로 통합 인증 상태 관리 final authState = ref.watch(supabaseAuthProvider); // 토큰 갱신 (MyIAM + Supabase) await ref.read(supabaseAuthProvider.notifier).refreshToken(); // 로그아웃 (MyIAM + Supabase) await ref.read(supabaseAuthProvider.notifier).logout(); ``` ### 인증 플로우 ```mermaid flowchart TD A["MyIAM 로그인
myiam_flutter_sdk의 LoginScreen/SignupScreen으로 OAuth2 인증"] A --> B["supabaseAuthProvider가 인증 감지
authNotifierProvider의 상태 변경을 자동으로 감시"] B --> C["Edge Function 호출
mint-supabase-token에 access_token을 전송하여 Supabase 세션 획득"] C --> D["Supabase Auth 로그인
setSession()으로 Supabase 서비스 접근 가능"] D --> E["자동 재연결
Supabase 세션 만료 시 MyIAM 토큰 갱신 후 Supabase 재발급"] E --> F["로그아웃
Supabase signOut() + MyIAM logout() 양쪽 모두 세션 정리"] ``` ## 수동 탈퇴 Edge Function 브릿지로 MyIAM + Supabase 데이터를 한 번에 정리 `MyiamAction.deregister`의 `onRedirect`에서 받은 token을 `SupabaseAuthNotifier.deregister(token)`에 전달하면, Edge Function이 MyIAM `completeDeregistration` 호출 + Supabase 데이터 purge + `auth.users` 삭제까지 원자적으로 처리한 뒤 MyIAM + Supabase 로그아웃으로 이어집니다. ### 흐름 개요 ```text 1. MyIAM이 deregisterRedirectUri로 ?token= 발송 2. 앱이 MyiamAction.deregister의 onRedirect(uri)에서 token 추출 3. ref.read(supabaseAuthProvider.notifier).deregister(token) 호출 ├─ SupabaseAuthBridge.deregisterWithToken(token) — Edge Function 호출 │ · MyIAM completeDeregistration │ · Supabase 데이터 purge │ · auth.users 삭제 └─ MyIAM + Supabase 로그아웃 (try/finally로 항상 실행) 4. DeregisterResult { redirectUrl, serviceUserUid } 반환 ``` ### 사용 예시 ```dart MyiamAction.deregister( completeRedirectUrl: config.deregisterRedirectUri, onRedirect: (uri) async { final token = uri.queryParameters['token']; if (token == null || token.isEmpty) return; final result = await ref .read(supabaseAuthProvider.notifier) .deregister(token); // result.redirectUrl — MyIAM 로그아웃 URL // result.serviceUserUid — 탈퇴된 MyIAM UID }, onBack: () => Navigator.of(context).pop(), ) ``` **필수:** 수동 탈퇴용 Edge Function(기본 이름 `deregister-callback`)을 Supabase 프로젝트에 배포해야 합니다. 테이블별 row purge는 서비스 스키마에 따라 다르므로, SDK에 포함된 `docs/edge-function-deregister.md` 템플릿을 기준으로 커스터마이즈하세요. ### 세션 drift 자동 감지 ```text // SupabaseAuthNotifier가 Supabase onAuthStateChange를 구독: // // 1. 세션 만료(signedOut) 감지 시: // → MyIAM 토큰 갱신 시도 → Supabase 세션 재발급 (자동 재연결) // → MyIAM 토큰도 만료되었으면 로그아웃 // // 2. 외부 revoke / 다른 기기 로그아웃 등: // → drift 방지를 위해 MyIAM 로그아웃까지 자동 체이닝 // // 별도 코드 불필요. ``` ## 커스터마이징 Edge Function 이름 변경, 별도 Supabase 인스턴스 ### Edge Function 이름 변경 기본 Edge Function 이름은 `mint-supabase-token`입니다. 다른 이름을 사용하려면 `supabaseAuthBridgeProvider`를 override하세요. `Edge Function 이름 변경` ```dart ProviderScope( overrides: [ authConfigProvider.overrideWithValue(/* ... */), supabaseAuthBridgeProvider.overrideWithValue( SupabaseAuthBridge(functionName: 'myCustomFunction'), ), ], child: const MyApp(), ) ``` ### Supabase 클라이언트 커스터마이징 별도의 Supabase 인스턴스를 사용할 수 있습니다. `Supabase 클라이언트 커스터마이징` ```dart supabaseAuthBridgeProvider.overrideWithValue( SupabaseAuthBridge( supabase: myCustomSupabaseClient, ), ) ``` ## Provider ### [타입] supabaseAuthProvider MyIAM + Supabase 통합 인증 상태 Provider. authNotifierProvider를 감시하여 MyIAM 인증 성공 시 자동으로 Supabase 세션 브릿지를 연결합니다. ```dart // 인증 상태에 따라 화면 전환 home: switch (ref.watch(supabaseAuthProvider)) { SupabaseAuthLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), SupabaseAuthAuthenticated() => const DashboardScreen(), SupabaseAuthUnauthenticated() => const HomeScreen(), SupabaseAuthError() => const HomeScreen(), } ``` `Supabase 연결 상태 확인` ```dart // Supabase 연결 상태 확인 final authState = ref.watch(supabaseAuthProvider); if (authState is SupabaseAuthAuthenticated) { if (authState.supabaseConnected) { // Supabase 서비스 사용 가능 } else if (authState.supabaseError != null) { // Supabase 연결 실패: authState.supabaseError } else { // Supabase 연결 중... } } ``` ### [타입] supabaseAuthBridgeProvider Supabase 세션 브릿지 서비스 Provider. Edge Function 이름이나 Supabase 인스턴스를 변경하려면 override하세요. ```dart final bridge = ref.read(supabaseAuthBridgeProvider); // 현재 Supabase 사용자 final user = bridge.currentUser; // Supabase 인증 상태 스트림 bridge.authStateChanges.listen((state) { // ... }); ``` ### [타입] supabaseClientProvider Supabase 클라이언트 인스턴스 Provider. Database, Storage 등 Supabase 서비스에 접근할 때 사용합니다. ```dart final client = ref.read(supabaseClientProvider); // Database final data = await client.from('notes').select(); // Storage final file = await client.storage.from('avatars').download('avatar.png'); ``` ## 타입 ### [타입] SupabaseAuthState MyIAM + Supabase 통합 인증 상태. sealed class로 패턴 매칭이 가능합니다. ```dart sealed class SupabaseAuthState {} class SupabaseAuthLoading extends SupabaseAuthState {} class SupabaseAuthUnauthenticated extends SupabaseAuthState {} class SupabaseAuthAuthenticated extends SupabaseAuthState { final MyiamTokens myiamTokens; final MyiamUser myiamUser; final bool supabaseConnected; final String? supabaseError; } class SupabaseAuthError extends SupabaseAuthState { final String message; } ``` #### 상태 | 속성명 | 설명 | | --- | --- | | SupabaseAuthLoading | 초기 로딩 | | SupabaseAuthUnauthenticated | 비인증 상태 | | SupabaseAuthAuthenticated | 인증됨 (MyIAM 토큰/사용자 + Supabase 연결 상태 포함) | | SupabaseAuthError | 에러 (message 포함) | `사용 예시` ```dart // 인증 상태에 따라 화면 전환 home: switch (ref.watch(supabaseAuthProvider)) { SupabaseAuthLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), SupabaseAuthAuthenticated() => const DashboardScreen(), SupabaseAuthUnauthenticated() => const HomeScreen(), SupabaseAuthError() => const HomeScreen(), } ``` ### [타입] SupabaseAuthAuthenticated 인증 완료 상태. MyIAM 사용자 정보와 Supabase 연결 상태를 포함합니다. ```dart class SupabaseAuthAuthenticated extends SupabaseAuthState { final MyiamTokens myiamTokens; final MyiamUser myiamUser; final bool supabaseConnected; final String? supabaseError; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | myiamTokens * | MyiamTokens | MyIAM access/refresh 토큰 | | myiamUser * | MyiamUser | MyIAM 사용자 정보 | | supabaseConnected | bool | Supabase 인증 완료 여부 (기본: false) | | supabaseError | String? | Supabase 연결 에러 메시지 | ### [타입] SupabaseAuthBridge MyIAM access_token을 Supabase 세션으로 교환하고 Supabase Auth에 로그인하는 브릿지 서비스. Edge Function을 통해 토큰 검증, UUID v5 매핑, 사용자 생성/동기화, 세션 발급을 수행합니다. ```dart class SupabaseAuthBridge { SupabaseAuthBridge({ SupabaseClient? supabase, String functionName = 'mint-supabase-token', String deregisterFunctionName = 'deregister-callback', }); } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | supabase | SupabaseClient? | Supabase 클라이언트 인스턴스. 생략 시 기본 인스턴스 사용 | | functionName | String | 로그인 Edge Function 이름 (기본: mint-supabase-token) | | deregisterFunctionName | String | 수동 탈퇴 Edge Function 이름 (기본: deregister-callback) | ### [타입] DeregisterResult SupabaseAuthBridge.deregisterWithToken() / SupabaseAuthNotifier.deregister()의 반환 타입. ```dart class DeregisterResult { final String redirectUrl; final String serviceUserUid; } ``` #### 필드 | 속성명 | 타입 | 설명 | | --- | --- | --- | | redirectUrl * | String | MyIAM 로그아웃 URL | | serviceUserUid * | String | 탈퇴된 MyIAM 사용자 UID | ## SupabaseAuthNotifier 메서드 ### [함수] refreshToken MyIAM 토큰을 갱신하고 Supabase에 재연결합니다. Supabase 세션 만료(signedOut) 감지 시 자동으로 호출됩니다. 이미 Supabase 연결된 상태에서 MyIAM 토큰만 갱신되면 중복 mint 없이 토큰만 업데이트합니다. MyIAM 서버 거부 시 자동 로그아웃되며, 네트워크 오류 시에는 세션을 유지합니다. `시그니처` ```dart supabaseAuthProvider.notifier.refreshToken() ``` **반환값:** `Future` `예시` ```dart // MyIAM 토큰 갱신 + Supabase 재연결 await ref.read(supabaseAuthProvider.notifier).refreshToken(); ``` ### [함수] logout Supabase signOut()을 먼저 실행한 후 MyIAM logout()을 호출합니다. Supabase signOut 실패와 무관하게 MyIAM logout은 항상 실행됩니다. `시그니처` ```dart supabaseAuthProvider.notifier.logout() ``` **반환값:** `Future` `예시` ```dart // MyIAM + Supabase 양쪽 모두 로그아웃 await ref.read(supabaseAuthProvider.notifier).logout(); ``` ### [함수] deregister 수동 탈퇴를 수행합니다. SupabaseAuthBridge.deregisterWithToken(token)을 호출한 뒤 try/finally로 MyIAM + Supabase 로그아웃까지 보장합니다. Edge Function 실패 시에도 로컬 세션은 반드시 정리되어 drift 상태를 차단합니다. `시그니처` ```dart supabaseAuthProvider.notifier.deregister(deregisterToken, {autoLogout}) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | deregisterToken * | String | MyiamAction.deregister의 onRedirect에서 추출한 ?token= 값 | | autoLogout | bool | 기본: true. false로 설정하면 Edge Function만 실행하고 로그아웃은 호출자가 직접 수행. 탈퇴 후 LogoutWebView로 세션 쿠키를 정리하는 흐름에 사용 | **반환값:** `Future` — Edge Function이 반환한 redirectUrl, serviceUserUid `예시` ```dart MyiamAction.deregister( completeRedirectUrl: config.deregisterRedirectUri, onRedirect: (uri) async { final token = uri.queryParameters['token']; if (token == null || token.isEmpty) return; final result = await ref .read(supabaseAuthProvider.notifier) .deregister(token); // result.redirectUrl — MyIAM 로그아웃 URL // result.serviceUserUid — 탈퇴된 MyIAM UID }, onBack: () => Navigator.of(context).pop(), ) ``` ## SupabaseAuthBridge 메서드 ### [함수] signInWithMyiamToken MyIAM access_token으로 Edge Function을 호출하여 Supabase 세션을 받고 Supabase Auth에 로그인합니다. 일반적으로 직접 호출할 필요 없이 supabaseAuthProvider가 자동으로 호출합니다. `시그니처` ```dart bridge.signInWithMyiamToken(accessToken) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | accessToken * | String | MyIAM access_token | **반환값:** `Future` — Supabase 인증 결과 ### [함수] deregisterWithToken 수동 탈퇴 Edge Function을 호출해 MyIAM completeDeregistration + Supabase 데이터 purge + auth.users 삭제까지 원자적으로 처리합니다. 로그아웃은 호출자가 직접 수행해야 하며, 일반적으로 SupabaseAuthNotifier.deregister()를 통해 호출하여 로그아웃까지 한 번에 처리합니다. `시그니처` ```dart bridge.deregisterWithToken(deregisterToken) ``` #### 매개변수 | 속성명 | 타입 | 설명 | | --- | --- | --- | | deregisterToken * | String | MyiamAction.deregister의 onRedirect에서 추출한 ?token= 값 | **반환값:** `Future` — Edge Function이 반환한 redirectUrl, serviceUserUid ### [함수] signOut Supabase에서 로그아웃합니다. `시그니처` ```dart bridge.signOut() ``` **반환값:** `Future` ### [함수] currentUser 현재 Supabase 인증 사용자를 반환합니다. `시그니처` ```dart bridge.currentUser ``` **반환값:** `User?` — Supabase 인증 사용자 또는 null ### [함수] authStateChanges Supabase 인증 상태 변경 스트림. `시그니처` ```dart bridge.authStateChanges ``` **반환값:** `Stream` — 인증 상태 변경 시 AuthState를 방출 --- ## Expo SDK API 레퍼런스 URL: https://myiam.io/docs/sdk/expo @myiam.io/expo-sdk — Expo(React Native) 앱을 위한 OAuth2/PKCE 인증 SDK 문서. 패키지: https://www.npmjs.com/package/@myiam.io/expo-sdk 릴리스 노트: https://myiam.io/docs/release-notes/expo-sdk # Expo SDK Expo(React Native) 앱을 위한 OAuth2/PKCE 인증 SDK. [Auth SDKReact Context 기반 OAuth2/PKCE 인증 상태 관리. 로그인·회원가입·로그아웃·토큰 갱신과 사용자 액션 훅을 제공합니다.](https://myiam.io/docs/sdk/expo/auth) [REST APIWeb SDK의 REST 클라이언트를 그대로 재사용합니다. useMyiam().api로 접근하며 메서드 시그니처가 동일합니다.](https://myiam.io/docs/sdk/web/server) --- ## Expo SDK Auth API 레퍼런스 URL: https://myiam.io/docs/sdk/expo/auth @myiam.io/expo-sdk - Expo 앱을 위한 OAuth2/PKCE 인증 SDK 문서. 패키지: https://www.npmjs.com/package/@myiam.io/expo-sdk 릴리스 노트: https://myiam.io/docs/release-notes/expo-sdk # Expo SDK Auth API 레퍼런스 Expo(React Native) 앱을 위한 OAuth2/PKCE 인증 SDK. 로그인·회원가입·로그아웃·토큰 갱신·사용자 액션을 지원합니다. ## 시작 가이드 설치, 빠른 시작, 인증 플로우 ### 설치 ```bash npx expo install expo-auth-session expo-web-browser expo-secure-store npm install @myiam.io/expo-sdk ``` `app.json`에 앱 스킴이 있어야 인증 후 앱으로 돌아올 수 있습니다. `app.json` ```json { "expo": { "scheme": "myiamsample", "ios": { "bundleIdentifier": "io.myiam.expo.sample" }, "android": { "package": "io.myiam.expo.sample" } } } ``` ### 빠른 시작 `tsx` ```react import { MyiamProvider, useMyiam } from "@myiam.io/expo-sdk" // 1. 초기화 (앱 최상단에서 1회) export default function App() { return ( ) } function Root() { // 2. 인증 상태 확인 const { status, user, login, signup, refresh, logout } = useMyiam() if (status === "loading") return if (status === "unauthenticated") { // 3. 로그인 / 회원가입 (취소하면 null) return