package core import ( "context" "crypto/x509" "encoding/json" "encoding/pem" "errors" "io" "net/http" "net/url" "strings" "time" "repositories.action2quare.com/ayo/gocommon/logger" "github.com/golang-jwt/jwt" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" ) type Apple_WebValidationTokenRequest struct { ClientID string ClientSecret string Code string RedirectURI string } type Apple_WebRefreshTokenRequest struct { ClientID string ClientSecret string RefreshToken string } type Apple_ValidationResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` IDToken string `json:"id_token"` Error string `json:"error"` ErrorDescription string `json:"error_description"` } func (mg *Maingate) platform_apple_get_login_url(w http.ResponseWriter, r *http.Request) { browserinfo, err := mg.GetUserBrowserInfo(r) if err != nil { w.WriteHeader(http.StatusBadRequest) logger.Error(err) return } existid := r.URL.Query().Get("existid") //fmt.Println("existid =>", existid) if existid != "" { // 기존 계정이 있는 경우에는 그 계정 부터 조회한다. info, err := mg.getUserTokenWithCheck(AuthPlatformApple, existid, browserinfo) if err == nil { if info.token != "" { params := url.Values{} params.Add("id", existid) params.Add("authtype", AuthPlatformApple) http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther) return } } } sessionkey := mg.GeneratePlatformLoginNonceKey() nonce := mg.GeneratePlatformLoginNonceKey() mg.mongoClient.Delete(CollectionPlatformLoginToken, bson.M{ "platform": AuthPlatformApple, "key": sessionkey, }) _, _, err = mg.mongoClient.Update(CollectionPlatformLoginToken, bson.M{ "_id": primitive.NewObjectID(), }, bson.M{ "$setOnInsert": bson.M{ "platform": AuthPlatformApple, "key": sessionkey, "nonce": nonce, "brinfo": browserinfo, }, }, options.Update().SetUpsert(true)) if err != nil { w.WriteHeader(http.StatusBadRequest) logger.Error(err) return } params := url.Values{} params.Add("client_id", config.AppleCientId) params.Add("redirect_uri", config.RedirectBaseUrl+"/authorize/"+AuthPlatformApple) params.Add("response_type", "code id_token") params.Add("scope", "name email") params.Add("nonce", nonce) params.Add("response_mode", "form_post") // set cookie for storing token cookie := http.Cookie{ Name: "LoginFlowContext_SessionKey", Value: sessionkey, Expires: time.Now().Add(1 * time.Hour), //SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteLaxMode, // HttpOnly: false, Secure: true, Path: "/", } http.SetCookie(w, &cookie) //Set-Cookie http.Redirect(w, r, "https://appleid.apple.com/auth/authorize?"+params.Encode(), http.StatusSeeOther) } func (mg *Maingate) platform_apple_authorize(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() body, _ := io.ReadAll(r.Body) bodyString := string(body) code := "" for _, params := range strings.Split(bodyString, "&") { args := strings.Split(params, "=") if len(args) == 2 { if args[0] == "code" { code = args[1] } } } // set cookie for storing token cookie := http.Cookie{ Name: "LoginFlowContext_code", Value: code, Expires: time.Now().Add(1 * time.Minute), SameSite: http.SameSiteLaxMode, Secure: true, Path: "/", } http.SetCookie(w, &cookie) http.Redirect(w, r, config.RedirectBaseUrl+"/authorize_result/"+AuthPlatformApple, http.StatusSeeOther) //-- 바로 받으니까 쿠키 안와서 한번 더 Redirect 시킨다. } func (mg *Maingate) platform_apple_authorize_result(w http.ResponseWriter, r *http.Request) { brinfo, err := mg.GetUserBrowserInfo(r) if err != nil { w.WriteHeader(http.StatusBadRequest) logger.Error(err) return } cookie, err := r.Cookie("LoginFlowContext_SessionKey") if err != nil { logger.Println("Session not found", err) w.WriteHeader(http.StatusBadRequest) return } cookiecode, err := r.Cookie("LoginFlowContext_code") if err != nil { logger.Println("code not found", err) w.WriteHeader(http.StatusBadRequest) return } code := cookiecode.Value found, err := mg.mongoClient.FindOne(CollectionPlatformLoginToken, bson.M{ "key": cookie.Value, "platform": AuthPlatformApple, }) if err != nil { logger.Println("LoginFlowContext_SessionKey find key :", err) w.WriteHeader(http.StatusBadRequest) return } if found == nil { logger.Println("LoginFlowContext_SessionKey not found") w.WriteHeader(http.StatusBadRequest) return } if cookie.Value != found["key"] { logger.Println("LoginFlowContext_SessionKey key not match") logger.Println(cookie.Value) logger.Println(found["key"]) w.WriteHeader(http.StatusBadRequest) return } if brinfo != found["brinfo"] { //-- 로그인 시작점과 인증점의 브라우저 혹은 접속지 정보가 다르다? logger.Println("LoginFlowContext_SessionKey brinfo not match ") logger.Println(brinfo) logger.Println(found["brinfo"]) w.WriteHeader(http.StatusBadRequest) return } // Generate the client secret used to authenticate with Apple's validation servers secret, err := generateClientSecret(config.ApplePrivateKey, config.AppleTeamId, config.AppleServiceId, config.AppleKeyId) if err != nil { logger.Error("error generating secret: ", err) return } vReq := Apple_WebValidationTokenRequest{ ClientID: config.AppleServiceId, ClientSecret: secret, Code: code, RedirectURI: config.RedirectBaseUrl + "/authorize/" + AuthPlatformApple, // This URL must be validated with apple in your service } var resp Apple_ValidationResponse err = verifyWebToken(context.Background(), vReq, &resp) if err != nil { logger.Error("error verifying: ", err) return } if resp.Error != "" { logger.Errorf("apple returned an error: %s - %s\n", resp.Error, resp.ErrorDescription) return } // fmt.Println("==============================") // fmt.Println("IDToken:", resp.IDToken) // fmt.Println("AccessToken:", resp.AccessToken) // fmt.Println("ExpiresIn:", resp.ExpiresIn) // fmt.Println("RefreshToken:", resp.RefreshToken) // fmt.Println("TokenType:", resp.TokenType) // fmt.Println("==============================") userid, email, nonce := JWTparseCode("https://appleid.apple.com/auth/keys", resp.IDToken) if nonce == "" || nonce != found["nonce"] { logger.Errorf("nonce not match") return } if userid != "" && email != "" { var info usertokeninfo info.platform = AuthPlatformApple info.userid = userid info.token = resp.RefreshToken info.brinfo = brinfo info.email = email mg.setUserToken(info) params := url.Values{} params.Add("id", userid) params.Add("authtype", AuthPlatformApple) http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther) } else { http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther) } } func (mg *Maingate) platform_apple_getuserinfo(refreshToken string) (bool, string, string) { //=================================RefreshToken을 사용해서 정보 가져 온다. 이미 인증된 사용자의 업데이트 목적 secret, err := generateClientSecret(config.ApplePrivateKey, config.AppleTeamId, config.AppleServiceId, config.AppleKeyId) if err != nil { logger.Error("error generating secret: ", err) return false, "", "" } vReqRefreshToken := Apple_WebRefreshTokenRequest{ ClientID: config.AppleServiceId, ClientSecret: secret, RefreshToken: refreshToken, } var respReferesh Apple_ValidationResponse err = verifyRefreshToken(context.Background(), vReqRefreshToken, &respReferesh) if err != nil { logger.Error("error verifying: " + err.Error()) return false, "", "" } if respReferesh.Error != "" { logger.Errorf("apple returned an error: %s - %s\n", respReferesh.Error, respReferesh.ErrorDescription) return false, "", "" } userid, email, _ := JWTparseCode("https://appleid.apple.com/auth/keys", respReferesh.IDToken) // fmt.Println("==============================") // fmt.Println("RefreshToken") // fmt.Println("==============================") // fmt.Println("IDToken:", respReferesh.IDToken) // fmt.Println("AccessToken:", respReferesh.AccessToken) // fmt.Println("ExpiresIn:", respReferesh.ExpiresIn) // fmt.Println("RefreshToken:", respReferesh.RefreshToken) // fmt.Println("TokenType:", respReferesh.TokenType) // fmt.Println("==============================") // fmt.Println("Parse:") // fmt.Println("userid:", userid) // fmt.Println("email:", email) // fmt.Println("nonce:", nonce) return true, userid, email } func generateClientSecret(signingKey, teamID, clientID, keyID string) (string, error) { block, _ := pem.Decode([]byte(signingKey)) if block == nil { return "", errors.New("empty block after decoding") } privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return "", err } // Create the Claims now := time.Now() claims := &jwt.StandardClaims{ Issuer: teamID, IssuedAt: now.Unix(), ExpiresAt: now.Add(time.Hour*24*180 - time.Second).Unix(), // 180 days Audience: "https://appleid.apple.com", Subject: clientID, } token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) token.Header["alg"] = "ES256" token.Header["kid"] = keyID return token.SignedString(privKey) } func verifyWebToken(ctx context.Context, reqBody Apple_WebValidationTokenRequest, result interface{}) error { data := url.Values{} data.Set("client_id", reqBody.ClientID) data.Set("client_secret", reqBody.ClientSecret) data.Set("code", reqBody.Code) data.Set("redirect_uri", reqBody.RedirectURI) data.Set("grant_type", "authorization_code") req, err := http.NewRequestWithContext(ctx, "POST", "https://appleid.apple.com/auth/token", strings.NewReader(data.Encode())) if err != nil { return err } req.Header.Add("content-type", "application/x-www-form-urlencoded") req.Header.Add("accept", "application/json") req.Header.Add("user-agent", "go-signin-with-apple") // apple requires a user agent client := &http.Client{} res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() return json.NewDecoder(res.Body).Decode(result) } func verifyRefreshToken(ctx context.Context, reqBody Apple_WebRefreshTokenRequest, result interface{}) error { data := url.Values{} data.Set("client_id", reqBody.ClientID) data.Set("client_secret", reqBody.ClientSecret) data.Set("grant_type", "refresh_token") data.Set("refresh_token", reqBody.RefreshToken) //return doRequest(ctx, c.client, &result, c.validationURL, data) req, err := http.NewRequestWithContext(ctx, "POST", "https://appleid.apple.com/auth/token", strings.NewReader(data.Encode())) if err != nil { return err } req.Header.Add("content-type", "application/x-www-form-urlencoded") req.Header.Add("accept", "application/json") req.Header.Add("user-agent", "go-signin-with-apple") // apple requires a user agent client := &http.Client{} res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() return json.NewDecoder(res.Body).Decode(result) }