지난번에 구글 Oauth2 인증을 고랭으로 구현하는 글을 적었는데
Facebook , Apple , Google 등 소셜로그인을 각 SDK 별로 별도로 구현하는 방법도 있겠지만
개발에 들어가는 시간과 노력이 너무 많이 들기에 찾아보다가 Cognito 라는 좋은 기능이 있어서 가져왔다.
Amazon Cognito 는 웹 및 모바일 앱에 대한 인증,권한 부여 및 사용자 관리를 제공한다.
이 글에서는 구현 위주로 설명을 하기 때문에 제대로된 설명을 보고 싶다면 다른 블로그를 살펴보면 되겠다.
바로 시작해보자.
아마존 콘솔 로그인 -> Cognito 검색 -> 사용자 풀 생성으로 들어가면
로그인 환경 구성에서 Cognito 사용자 풀은 디폴트로 설정이 되어있다.
옆에 연동 자격 증명 공급자 라는 탭이 바로 우리가 쉽게 구현할 수 있는 소셜 로그인 구현이라고 보면 된다.
지금 클릭하고 넘어가도 되지만 연동 자격 증명 공급자는 나중에도 설정을 할수 있으니
사용자 풀 로그인 옵션에서 사용하고자 하는 걸 클릭에서 다음으로 넘어가자. (필자는 이메일만 필요로 해서 이메일만 선택했다.)
보안 요구사항 구성으로는 기본값이 있고 사용자 지정으로 최소 6자의 간단한 비밀번호를 생성하게 설정 할 수 있다.
필자는 기본값으로 설정했다.
MFA 는 설정하지 않았고 나머지도 다 기본으로 해서 다음으로 넘어가자
필수 속성에서 추가 필수 속성을 선택할수 있다.
주소,생일,성별,이름,닉네임,폰번호,프로필 사진등 관리하고 싶은 항목들을 선택해 추가해주면
회원가입을 할시 해당 속성들을 기입하게 할 수 있다.
필자는 나머지 데이터는 여기서 수집하는게 아닌 앱 내부에서 수집하기에 따로 추가 하지 않았다.
코그니토 사용자 풀로 가입을 할시 인증코드를 보낼때 사용하는 부분이다.
기존에 있을시 사용하고 아니면 SES 나 Cognito 를 새로 설정해서 사용해줘도 된다.
우리는 소셜 로그인을 사용할 것이기 때문에 호스팅 인증페이지 체크박스를 체크해주고 Cognito 도메인을 생성해준다.
(원하는 이름으로 하면됨)
모바일 웹앱을 사용할것이기 때문에 퍼블릭 클라이언트를 지정해주고 URL 은 후에 실제로 사용하는 콜백 URL 을 설정해주면 되지만
지금은 테스트 용도이니 필자는 golang 으로 5000번 포트를 여는 웹 어플리케이션을 열고 테스트 해볼 예정이라 임시로 작성했다.
고급 앱 클라이언트 사용설정에서 토큰 갱신 만료는 1년으로 하고 나머지는 다 24시간으로 설정해줬다.
여기서 자격 증명 공급자는 우리가 처음 위에서 Facebook , Google , Apple 을 선택하지 않았으니
Cognito 사용자 풀만 나타나게 되어있는데 사용자 풀을 생성한 후에 다시 설정이 가능하니 일단 넘어가도 좋다.
검토후에 생성을 하게 되면 아래와 같이 사용자 풀이 만들어진걸 볼수 있다.
사용자 풀을 생성했으니 여기에 이제 자격 증명 공급자를 추가해야 한다. ( 소셜 로그인 )
사용자풀이름을 클릭하고 로그인 경험으로 오게 되면 아래와 같이 자격 증명 공급자 추가가 있다.
한번에 한번씩 추가가 가능하니 이글에서는 Google 만 추가해보도록 하겠다.
여기서에서 우린 구글 클라이언트 ID 와 보안키가 필요한데
Google Cloud Platform 에서 프로젝트를 생성한후 사용자 인증정보를 설정해야 한다.
AWS 창은 잠시 놔두고 새 웹을 열고 GCP 로 들어간다.
승인된 도메인은 amazoncognito.com 을 넣어주고 저장한다.
다음엔 사용자 인증정보를 만들어야 한다.
Oauth 클라이언트 ID 를 설정해주고
승인된 자바스크립트 원본은 아까 Cognito 도메인을 입력해주고
리디렉션 URI 는 Cognito 도메인에 /oauth2/idpresponse 를 붙여주면 된다.
EX ) 승인된 자바스크립트 원본 : https://내앱이름.auth.ap-northeast-2.amazoncognito.com
리디렉션 URI : https://내앱이름.auth.ap-northeast-2.amazoncognito.com/oauth2/idpresponse
여기서 저장을 누르게 되면 구글 클라이언트 ID 와 보안키를 받게 되는데 이 값을
아래에 넣고 권한 부여범위는 profile email openid 풀속성 매핑은 email로 한후 생성한다.
이제 Google 자격 증명 공급자 로그인이 추가가 되었다.
이후 가입경험 탭으로 가서
사용자 지정 속성에서 위와 같은 string 타입의 access_token 속성을 추가한다.
이후 앱통합 탭으로 이동한 후
스크롤 맨밑 앱 클라이언트 목록으로 들어간다
호스팅 UI 편집 버튼을 누른후
아까 추가해주지 않았던 Google 자격 증명 공급자를 선택한후 저장한다.
호스팅 UI 에서 아래와 같이 나타나 있으면 현재까지 잘 따라온거고 호스팅 UI 보기를 클릭하면
우리는 사용자가 이메일 패스워드를 통해 로그인 할수있는것 하나와 Google 자격 증명 공급자가 나타난걸 볼수있다.
이제부터 우리는 로컬에서 웹서버를 열고 코딩을 해야한다.
Google login 을 하게되면 우리가 콜백URL 을 localhost:5000을 설정해놨기 때문에
이렇게 에러가 뜨게 된다. 하지만 URL 을 잘보면 뒤에 code 값이 쿼리로 날라온걸 볼수 있다.
--- 잠깐 설명 타임 ---
위에서 받은 code 와 grant_type , client_id , redirect_uri 를 바디값으로
우리가 정의한 Cognito 도메인 + /oauth2/token 으로 POST 리퀘스트를 날리게 되면
리스폰스로 아래와 같은 값을 받게 된다.
IdToken string `json:"id_token"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
우리는 이 토큰 값으로 검증하고 우리 앱 백엔드나 프론트에 넣어서 로그인 기능을 사용할수 있게 된다.
저 토큰은 JWT 이기 때문에 해당 토큰을 사용하려면 JWT 에 대한 기본적인 지식을 알고 있어야 한다.
우리는 사용자풀을 생성했고 메인에서 사용자 풀 ID 를 확인 할 수있다.
region 은 이글을 읽는 모두는 한국인일테니 ap-northeast-2 일테고
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
위의 도메인에 해당 region 과 user-pool-id 를 넣으면
type JWK struct {
Keys []struct {
Alg string `json:"alg"`
E string `json:"e"`
Kid string `json:"kid"`
Kty string `json:"kty"`
N string `json:"n"`
} `json:"keys"`
}
위와 같은 json 타입을 반환하게 된다.
우리는 여기에서 E 와 N 을 이용해서 JWT 을 base64URLEncoding 으로 PublicKey를 생성해
JWT 를 파싱할수 있게 되고 우리는 파싱한 JWT 의 정보들을 가지고 인증을 하고 로그인 유지를 하게 된다.
---
Cognito 도메인은 아까 우리가 넣은 값이므로 여기에 저장되어있고
클라이언트 아이디는 앱 클라이언트 목록안에 들어가면 볼수 있다.
grant_type 은 토큰을 사용하게 되면 authorization_code 디폴트로 넣어주면 된다.
아마 고를 사용하시는 분들이 많이 없을것이기 때문에
기본적인 http 통신을 알고 JWT 을 아시는 분들은 여기서는 사용하는 편한 언어에 맞게 혼자 하셔도 무방 할것 같다.
아래에는 필자가 작성한 코드 스니펫이다.
package cognito
import (
"bytes"
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"io/ioutil"
"letus.gg/rest/config"
"math/big"
"net/http"
"net/url"
)
type CognitoResponse struct {
IdToken string `json:"id_token"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
type JWK struct {
Keys []struct {
Alg string `json:"alg"`
E string `json:"e"`
Kid string `json:"kid"`
Kty string `json:"kty"`
N string `json:"n"`
} `json:"keys"`
}
const cognitoIdp = "https://cognito-idp.ap-northeast-2.amazonaws.com"
func Authorization(c *gin.Context) {
code := c.Query("code")
bodyData := url.Values{
"grant_type": {"authorization_code"},
"client_id": {config.GetString("COGNITO_CLIENT_ID")},
"code": {code},
"redirect_uri": {"http://localhost:5000"},
}
client := &http.Client{}
// request cognito token
req, err := http.NewRequest("POST", fmt.Sprintf("%s/oauth2/token",config.GetString("COGNITO_DOMAIN")), bytes.NewBufferString(bodyData.Encode()))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var cr CognitoResponse
if err := json.Unmarshal(body, &cr); err != nil {
panic(err)
}
// request jwk
resp, err = http.Get(fmt.Sprintf("%s/%s/.well-known/jwks.json",cognitoIdp,config.GetString("USER_POOL_ID")))
if err != nil {
panic(err)
}
defer resp.Body.Close()
body , err = ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var jwk JWK
if err = json.Unmarshal(body,&jwk); err != nil {
panic(err)
}
token, err := parseJWT(cr.IdToken,jwk.Keys[1].E,jwk.Keys[1].N)
fmt.Println("----Id Token Info------")
fmt.Println(token.Claims)
fmt.Println("----Id Token------")
token , err = parseJWT(cr.AccessToken,jwk.Keys[1].E,jwk.Keys[1].N)
fmt.Println("----Access Token Info------")
fmt.Println(token.Claims)
fmt.Println("----Access Token Info------")
c.JSON(http.StatusOK, &cr)
}
func parseJWT(tokenString,e,n string) (*jwt.Token, error) {
token , err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
key := convertKey(e,n)
return key , nil
})
if err != nil {
return token,err
}
return token,nil
}
func convertKey(rawE , rawN string) *rsa.PublicKey {
decodeE, err := base64.RawURLEncoding.DecodeString(rawE)
if err != nil {
panic(err)
}
if len(decodeE) < 4 {
nData := make([]byte,4)
copy(nData[4-len(decodeE):],decodeE)
decodeE = nData
}
pubKey := &rsa.PublicKey{
N:&big.Int{},
E:int(binary.BigEndian.Uint32(decodeE[:])),
}
decodedN, err := base64.RawURLEncoding.DecodeString(rawN)
if err != nil {
panic(err)
}
pubKey.N.SetBytes(decodedN)
return pubKey
}
서버를 열고 코그니토에서 로그인시도를 해보면 id_token 과 access_token 의 정보를 가져올수 있다.
마치면서.
이렇게 aws Cognito 에 대해 구현을 해봤고 golang 코드로 인증하는 부분까지 해봤다.
로그인을 하려면 인증한 claims에 있는 정보들로 생각하면서 구현해볼수 있다.
공부하면서 글을 적은것이기 때문에 글에 오류가 있을수 있지만 소셜로그인이나 회원가입을 쉽게 구현하고 싶은분에게 도움이 됐으면 한다.
'AWS' 카테고리의 다른 글
VPC 피어링 (0) | 2022.11.03 |
---|---|
aws ec2 ssh 접속 오류 (0) | 2022.10.23 |
S3 영상이 재생이 안되고 다운로드가 된다? (0) | 2022.04.27 |
Bastion Host 로 EC2 우회 접속 스크립트 설정 (0) | 2022.04.20 |
CloudFront 사용시 주의할 점 (0) | 2022.04.15 |