ํ”ผ๋“œ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
Angular OAuth2 Flow ๐Ÿซง - ์ธ๋„ค์ผ
133 reads
ยท
2024/11/23 02:33

Angular OAuth2 Flow ๐Ÿซง

OAuth2 ๊ธฐ๋ฐ˜์˜ ์ธ์ฆ์„ ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์ธ์ฆ ํ๋ฆ„

image

์„ค์ •

๋จผ์ € @angular-flow/oauth2 ๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

npm i @angular-flow/oauth2

app.config.ts ์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

// app.config.ts
...
export const appConfig: ApplicationConfig = {
  providers: [
    ...
    // oauth2 ๊ณต๊ธ‰์ž ์ถ”๊ฐ€
    provideOAuth2({
      refreshBehavior: RefreshTokensUsecase // RefreshBehavior๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค ๋“ฑ๋ก
      // tokenStorage: <TokenStorage ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค> (์„ ํƒ) 
      // ํ•˜์ด๋ธŒ๋ฆฌ๋“œ์›น์•ฑ์—์„œ๋Š” ์ œ๊ณต๋˜๋Š” `CapacitorStorage`๋กœ ๋“ฑ๋กํ•ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
    }),
    provideHttpClient(
      withInterceptors([
        // oauth2 ์ธํ„ฐ์…‰ํ„ฐ ์ถ”๊ฐ€
        oauth2FlowInterceptor
      ])
    ),
  ]
};

๋˜๋Š” httpClient ๋ฅผ ํฌํ•จํ•˜๋Š” ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideOAuth2WithHttpClient({
      refreshBehavior: RefreshTokensUsecase, // ํ•„์ˆ˜๋“ฑ๋ก
      interceptors: authMockInterceptors, // ์„ ํƒ์‚ฌํ•ญ
    }),
  ],
};

OAuth2 ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•ด์„œ refreshBehavior ๋“ฑ๋ก์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.

RefreshBehavior ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  refresh ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜ํƒ€์ž…์„ ๋งŒ์กฑํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

  • refresh ๋ฉ”์„œ๋“œ๋Š” OAuth2 ๋‚ด๋ถ€ ์‹œ์Šคํ…œ์—์„œ ํ˜ธ์ถœ ๋ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ ์ธ์ž๊ฐ’์œผ๋กœ ํ˜„์žฌ ์‹œ์ ์˜ refreshToken์„ ๋„˜๊ธฐ๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๊ฐ’์„ ํ™œ์šฉํ•˜์—ฌ ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@Injectable()
export class RefreshTokensUsecase implements RefreshBehavior {
  private readonly httpClient = inject(HttpClient);

  // @angular-flow/oauth2 ์—์„œ ์„ค์ •๋œ ํ† ํฐ ์Šคํ† ๋ฆฌ์ง€ ์ฃผ์ž…
  private readonly tokenStorage = inject(TOKEN_STORAGE);

  refresh(refreshToken: string): Observable<TokenResource> {
    // ๋ฐฑ์—”๋“œ์™€ ์•ฝ์†๋œ ๋ฐฉ์‹์œผ๋กœ http ํ˜ธ์ถœ
    const header = new HttpHeaders()
      .set("Authorization", `Bearer ${refreshToken}`);
    return this.httpClient.post<TokenResource>(REFRESH_TOKEN_MOCK_URL, {},{
          headers: header,
          // โœ… context์— skipOAuth2Flow ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
          // oauth2Flow ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ์ง„ํ–‰ํ•˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
          context: skipOAuth2Flow,
    }).pipe(
        // ์š”์ฒญ ์‹คํŒจ ์‹œ ํ›„์ฒ˜๋ฆฌ
        catchError((res: HttpErrorResponse) => {
          window.alert(res.error.message);
          return throwError(() => res);
        })
    );
  }
}

ํ† ํฐ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ํ†ตํ•ด ํ† ํฐ์„ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์ €์žฅ, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ† ํฐ ๋ฆฌ์†Œ์Šค๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ „๋‹ฌ ๋ฐ›์„ ๊ฒฝ์šฐ ์ œ๊ณต๋˜๋Š” TOKEN_STORAGE ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ† ํฐ ๋ฆฌ์†Œ์Šค๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  • ํ† ํฐ์Šคํ† ๋ฆฌ์ง€๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์ €์žฅ์†Œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Capacitor Preference ๊นŒ์ง€ ๋Œ€์‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ œ๊ณต๋˜๋Š” ๋ฉ”์„œ๋“œ๋“ค์€ ๋ชจ๋‘ Promise ๋ฐ˜ํ™˜ํƒ€์ž…์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
@Injectable()
export class LoginUsecase {
  private readonly httpClient = inject(HttpClient);

  private readonly tokenStorage = inject(TOKEN_STORAGE);

  async execute(dto: ReqLoginDTO) {
    // http ์š”์ฒญ
    const http$ = this.httpClient.post<TokenResource>(LOGIN_MOCK_URL, dto).pipe(
      catchError((res: HttpErrorResponse) => {
        window.alert(res.error.message);
        return throwError(() => res);
      })
    );
    const res = await lastValueFrom(http$);

    // ํ† ํฐ์Šคํ† ๋ฆฌ์ง€์— ์‘๋‹ต๊ฐ’ ๋ฐ˜ํ™˜
    await this.tokenStorage.set(res);
  }
}

์ด์ œ ์ „์—ญ์— ๋“ฑ๋ก๋œ httpClient ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ api ์š”์ฒญ์„ ์ง„ํ–‰ํ•  ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ผ๋“ค์ด ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.

  • (ํ† ํฐ์žฌ๋ฐœ๊ธ‰์„ ์ œ์™ธํ•œ) ๋ชจ๋“  API ์š”์ฒญ์— accessToken ์œ ๋ฌด์— ๋”ฐ๋ผ Authorization ํ•ด๋”์— Bearer ํ˜•ํƒœ๋กœ ์—‘์„ธ์Šคํ† ํฐ์„ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
  • oauth2 ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๊ฑฐ์น˜๋Š” ์š”์ฒญ๋“ค์€ ์‹คํŒจ ์‘๋‹ต(401 Unauthorization error) ์‹œ ๋“ฑ๋ก๋œ ํ† ํฐ์žฌ๋ฐœ๊ธ‰ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์š”์ฒญ์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    • ์‹คํŒจํ–ˆ๋˜ ์š”์ฒญ๋“ค์€ ๋Œ€๊ธฐ์—ด ํ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  • ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์š”์ฒญ์ด ์‹คํŒจ๋กœ ๋๋‚  ๊ฒฝ์šฐ, ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์—์„œ ํ›„์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์‹คํŒจ ์‹œ oauth2 ์„œ๋น„์Šค์—์„œ ํ† ํฐ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์ž๋™์œผ๋กœ ๋น„์›๋‹ˆ๋‹ค.
  • ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์š”์ฒญ์ด ์„ฑ๊ณต์ ์ด๋ฉด ํ† ํฐ ๋ฆฌ์†Œ์Šค๋ฅผ ์ž๋™์œผ๋กœ ํ† ํฐ ์Šคํ† ๋ฆฌ์ง€์— ๋‹ค์‹œ ์ €์žฅํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
    • ๋Œ€๊ธฐ์—ด ํ์— ์ €์žฅ๋œ ์š”์ฒญ๋“ค๋„ ์ผ๊ด„์ ์œผ๋กœ ๋‹ค์‹œ ์š”์ฒญํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.