2023-05-24 12:16:03 +09:00
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/x509"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"encoding/pem"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2023-05-24 15:31:01 +09:00
|
|
|
"repositories.action2quare.com/ayo/gocommon/logger"
|
2023-05-24 12:16:03 +09:00
|
|
|
|
|
|
|
|
"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", mg.AppleCientId)
|
|
|
|
|
params.Add("redirect_uri", mg.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, mg.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(mg.ApplePrivateKey, mg.AppleTeamId, mg.AppleServiceId, mg.AppleKeyId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("error generating secret: ", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vReq := Apple_WebValidationTokenRequest{
|
|
|
|
|
ClientID: mg.AppleServiceId,
|
|
|
|
|
ClientSecret: secret,
|
|
|
|
|
Code: code,
|
|
|
|
|
RedirectURI: mg.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
|
|
|
|
|
|
|
|
|
|
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(mg.ApplePrivateKey, mg.AppleTeamId, mg.AppleServiceId, mg.AppleKeyId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("error generating secret: ", err)
|
|
|
|
|
return false, "", ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vReqRefreshToken := Apple_WebRefreshTokenRequest{
|
|
|
|
|
ClientID: mg.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 != "" {
|
2023-06-16 14:41:45 +09:00
|
|
|
logger.Errorf("apple returned an error: %s - %s\n", respReferesh.Error, respReferesh.ErrorDescription)
|
2023-05-24 12:16:03 +09:00
|
|
|
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)
|
|
|
|
|
}
|