OAuth 2.0 직접 구현해보기

Sammy·2024년 4월 18일
0

Authorization

목록 보기
9/9
  1. google OAuth 2.0 인증 정보 만들기

  2. 만든 후 인증 JSON 파일 다운로드

    {
      "web": {
        "client_id": "718453155317-***.apps.googleusercontent.com",
        "project_id": "sammy-oauth",
        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
        "token_uri": "https://oauth2.googleapis.com/token",
        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
        "client_secret": "GOCSPX-***",
        "redirect_uris": ["http://localhost:3000/auth/google/callback"],
        "javascript_origins": ["http://localhost:3000"]
      }
    }
  3. env 파일에 client_id 와 client_secret 설정

    GOOGLE_CLIENT_ID=718453155317-***.apps.googleusercontent.com
    GOOGLE_CLIENT_SECRET=GOCSPX-***
  4. go 프로젝트 생성

  5. googleLoginHandler 구현
    a. API 엔드포인트는 http://localhost:3000/auth/google/login 로 설정
    b. Cookie 에 state 값 셋팅
    - 랜덤한 숫자로 state 값을 만드는데 해당 값은 리다이렉트 될 때 검증하기 위해 한번 쓰고 버려지는 임의의 문자열 key이다. (csrf 공격을 방지하기 위함)
    - state 값을 oauthstate 라는 이름으로 24시간 유효기간의 cookie 셋팅
    c. https://accounts.google.com/o/oauth2/auth 로 리다이렉트

  6. googleAuthCallback Handler 구현
    a. API 엔드포인트는 http://localhost:3000/auth/google/callback으로 설정
    b. state 값과 쿠키에 담긴 oauthstate 값이 일치하는지 확인
    c. Form 에 담긴 code값으로 Access Token Exchange
    - 함수 내에서 https://oauth2.googleapis.com/token 호출
    d. Access Token 값으로 api 호출해서 userinfo 가져온다.
    - https://www.googleapis.com/oauth2/v2/userinfo?access_token={acceess_token}

최종 구현

  1. localhost:3000 에 접근해서 Google Login 버튼 클릭

    https://accounts.google.com/o/oauth2/auth 요청 query parameter

    client_id: 718453155317-***.apps.googleusercontent.com
    redirect_uri: http://localhost:3000/auth/google/callback
    response_type: code
    scope: https://www.googleapis.com/auth/userinfo.email
    state: lWbHTRADfE17uwQH0eLGSQ==
  2. 계정 선택

  3. 계속 클릭

  4. http://localhost:3000/auth/google/callback 리다이렉트 되고 아래 정보를 받아온다.

    state: lWbHTRADfE17uwQH0eLGSQ==
    code: 4/0AeaYSHAUlE2KqtavTS1VHBhC2G_Wv2In580sXAhFb5DZudYbp_c_ME1BOYL2H8365Oy-ng
    scope: email https://www.googleapis.com/auth/userinfo.email openid

    state 값을 검증하고 code 값으로 https://oauth2.googleapis.com/token 호출하여 access token 을 받아온다.
    그리고 해당 토큰값으로 https://www.googleapis.com/oauth2/v2/userinfo?access_token=${access_token} 호출하여 userinfo 를 받아온다.


golang 코드

var (
	googleOAuthConfig = oauth2.Config{
		ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
		ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
		RedirectURL:  "http://localhost:3000/auth/google/callback",
		Scopes:       []string{"https://www.googleapis.com/auth/userinfo.email"},
		Endpoint:     google.Endpoint,
	}
)

func main() {

	mux := pat.New()
	mux.HandleFunc("/auth/google/login", googleLoginHandler)
	mux.HandleFunc("/auth/google/callback", googleAuthCallback)

	n := negroni.Classic()
	n.UseHandler(mux)

	http.ListenAndServe(":3000", n)
}

func googleAuthCallback(w http.ResponseWriter, r *http.Request) {
	oauthstate, _ := r.Cookie("oauthstate")

	if r.FormValue("state") != oauthstate.Value {
		log.Printf("invalid google oauth state! Cookie: %s, State: %s \n", oauthstate.Value, r.FormValue("state"))
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		return
	}

	data, err := getGoogleUserInfo(r.FormValue("code"))
	if err != nil {
		log.Println(err.Error())
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		return
	}

	fmt.Fprintf(w, string(data))
}

var oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="

func getGoogleUserInfo(code string) ([]byte, error) {
	token, err := googleOAuthConfig.Exchange(context.Background(), code)
	if err != nil {
		return nil, fmt.Errorf("Failure to Exchange %s \n", err.Error())
	}

	resp, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
	if err != nil {
		return nil, fmt.Errorf("Failed to Get UserInfo %s \n", err.Error())
	}

	return ioutil.ReadAll(resp.Body)
}

func googleLoginHandler(w http.ResponseWriter, r *http.Request) {
	state := generateStateOauthCookie(w)
	url := googleOAuthConfig.AuthCodeURL(state) // 리다이렉트 될 때 검증하는 한번 쓰고 버려지는 key, csrf 방지를 위함.
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func generateStateOauthCookie(w http.ResponseWriter) string {
	expiration := time.Now().Add(24 * time.Hour)

	b := make([]byte, 16)
	rand.Read(b)
	state := base64.URLEncoding.EncodeToString(b)

	cookie := &http.Cookie{
		Name:    "oauthstate",
		Value:   state,
		Expires: expiration,
	}
	http.SetCookie(w, cookie)
	return state
}

++ 추가 공부

Client Secret 의 용도

  • client_id

    • URL에 노출되는 공개 식별자이다.
    • 보안을 위한 것이 아니라 단순히 클라이언트를 식별하기 위한 값이다.
    • 공개되더라도 제3자가 추측할 수 없는 것이 좋다.
    • 32자리 16진수 문자열 형태로 사용한다.
  • client_secret

    • 애플리케이션과 인증 서버에만 알려진 client_id 에 대한 비밀키이다.
    • 절대 노출해서는 안된다.
    • 공격자가 클라이언트를 가장하여 리소스 서버에 접근하는 것을 어렵게 만들 수 있다.
    • 해시 함수를 사용해 추측할 수 없을 만큼 무작위로 생성해야한다.
    • 노출되었을 경우엔 교체해야한다.
profile
Web Developer

0개의 댓글