maingate 이전

This commit is contained in:
2023-05-24 12:16:03 +09:00
commit 1fdfe3f45b
48 changed files with 5677 additions and 0 deletions

472
core/api.go Normal file
View File

@ -0,0 +1,472 @@
package core
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"sort"
"strings"
"sync/atomic"
"time"
"unsafe"
"repositories.action2quare.com/ayo/go-ayo/common"
"repositories.action2quare.com/ayo/go-ayo/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
func (caller apiCaller) isGlobalAdmin() bool {
if *noauth {
return true
}
email, ok := caller.userinfo["email"]
if !ok {
return false
}
if _, ok := caller.admins[email.(string)]; ok {
return true
}
return false
}
func (caller apiCaller) writeAccessableServices(w http.ResponseWriter) {
services, editable := caller.getAccessableServices()
for _, r := range editable {
w.Header().Add("MG-X-SERVICE-EDITABLE", r)
}
w.Write([]byte("{"))
start := true
for _, v := range services {
if !start {
w.Write([]byte(","))
}
w.Write([]byte(fmt.Sprintf(`"%s":`, v.ServiceName)))
serptr := atomic.LoadPointer(&v.serviceSerialized)
w.Write(*(*[]byte)(serptr))
start = false
}
w.Write([]byte("}"))
}
func (caller apiCaller) getAccessableServices() ([]*serviceDescription, []string) {
allservices := caller.mg.services.all()
v, ok := caller.userinfo["email"]
if !ok {
return nil, nil
}
email := v.(string)
_, admin := caller.admins[email]
var output []*serviceDescription
var editable []string
for _, desc := range allservices {
if admin {
output = append(output, desc)
editable = append(editable, desc.ServiceName)
} else if desc.isValidAPIUser("*", email) {
output = append(output, desc)
if desc.isValidAPIUser("service", email) {
editable = append(editable, desc.ServiceName)
}
}
}
sort.Slice(output, func(i, j int) bool {
return output[i].ServiceName < output[j].ServiceName
})
return output, editable
}
func (caller apiCaller) isValidUser(service any, category string) (valid bool, admin bool) {
if *noauth {
return true, true
}
v, ok := caller.userinfo["email"]
if !ok {
logger.Println("isVaidUser failed. email is missing :", caller.userinfo)
return false, false
}
email := v.(string)
if _, ok := caller.admins[email]; ok {
return true, true
}
svcdesc := caller.mg.services.get(service)
if svcdesc == nil {
logger.Println("isVaidUser failed. service is missing :", service)
return false, false
}
return svcdesc.isValidAPIUser(category, email), false
}
func (caller apiCaller) whitelistAPI(w http.ResponseWriter, r *http.Request) error {
mg := caller.mg
queryvals := r.URL.Query()
if r.Method == "GET" {
service := queryvals.Get("service")
if valid, _ := caller.isValidUser(service, "whitelist"); !valid {
logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
return nil
}
if len(service) > 0 {
all, err := mg.mongoClient.FindAll(CollectionWhitelist, bson.M{
"service": service,
})
if err != nil {
return err
}
if len(all) > 0 {
allraw, _ := json.Marshal(all)
w.Write(allraw)
}
} else {
logger.Println("service param is missing")
}
} else if r.Method == "PUT" {
body, _ := io.ReadAll(r.Body)
var member whitelistmember
if err := json.Unmarshal(body, &member); err != nil {
return err
}
if valid, _ := caller.isValidUser(member.Service, "whitelist"); !valid {
logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
return nil
}
member.Expired = 0
_, _, err := mg.mongoClient.Update(CollectionWhitelist, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$set": &member,
}, options.Update().SetUpsert(true))
if err != nil {
return err
}
} else if r.Method == "DELETE" {
id := queryvals.Get("id")
if len(id) == 0 {
return errors.New("id param is missing")
}
idobj, err := primitive.ObjectIDFromHex(id)
if err != nil {
return err
}
_, _, err = mg.mongoClient.Update(CollectionWhitelist, bson.M{
"_id": idobj,
}, bson.M{
"$currentDate": bson.M{
"_ts": bson.M{"$type": "date"},
},
}, options.Update().SetUpsert(false))
if err != nil {
return err
}
}
return nil
}
func (caller apiCaller) serviceAPI(w http.ResponseWriter, r *http.Request) error {
mg := caller.mg
queryvals := r.URL.Query()
if r.Method == "GET" {
name := queryvals.Get("name")
if len(name) > 0 {
if valid, _ := caller.isValidUser(name, "*"); !valid {
logger.Println("serviceAPI failed. not vaild user :", r.Method, caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
return nil
}
if valid, admin := caller.isValidUser(name, "service"); valid || admin {
w.Header().Add("MG-X-SERVICE-EDITABLE", name)
}
serptr := atomic.LoadPointer(&mg.services.get(name).serviceSerialized)
w.Write(*(*[]byte)(serptr))
} else {
caller.writeAccessableServices(w)
}
} else if r.Method == "POST" {
body, _ := io.ReadAll(r.Body)
var service serviceDescription
if err := json.Unmarshal(body, &service); err != nil {
return err
}
if service.Id.IsZero() {
if caller.isGlobalAdmin() {
service.Id = primitive.NewObjectID()
} else {
logger.Println("serviceAPI failed. not vaild user :", r.Method, caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
return nil
}
} else if valid, _ := caller.isValidUser(service.Id, "service"); !valid {
logger.Println("serviceAPI failed. not vaild user :", r.Method, caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
return nil
}
filter := bson.M{"_id": service.Id}
success, _, err := mg.mongoClient.Update(CollectionService, filter, bson.M{
"$set": &service,
}, options.Update().SetUpsert(true))
if err != nil {
return err
}
if !success {
logger.Println("serviceAPI failed. not vaild user :", caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
}
}
return nil
}
func (caller apiCaller) accountAPI(w http.ResponseWriter, r *http.Request) error {
mg := caller.mg
queryvals := r.URL.Query()
if r.Method == "GET" {
service := queryvals.Get("service")
if len(service) == 0 {
return nil
}
if valid, _ := caller.isValidUser(service, "account"); !valid {
logger.Println("accountAPI failed. not vaild user :", r.Method, caller.userinfo)
w.WriteHeader(http.StatusBadRequest)
return nil
}
var accdoc primitive.M
if v := queryvals.Get("accid"); len(v) == 0 {
email := queryvals.Get("email")
platform := queryvals.Get("platform")
if len(email) == 0 || len(platform) == 0 {
return nil
}
found, err := mg.mongoClient.FindOne(CollectionLink, bson.M{
"email": email,
"platform": platform,
})
if err != nil {
return err
}
if found == nil {
return nil
}
if idobj, ok := found["_id"]; ok {
svcdoc, err := mg.mongoClient.FindOne(common.CollectionName(service), bson.M{
"_id": idobj,
})
if err != nil {
return err
}
if svcdoc != nil {
found["accid"] = svcdoc["accid"]
}
accdoc = found
}
} else {
accid, err := primitive.ObjectIDFromHex(v)
if err != nil {
return err
}
svcdoc, err := mg.mongoClient.FindOne(common.CollectionName(service), bson.M{
"accid": accid,
})
if err != nil {
return err
}
found, err := mg.mongoClient.FindOne(CollectionLink, bson.M{
"_id": svcdoc["_id"],
})
if err != nil {
return err
}
if found != nil {
found["accid"] = accid
}
accdoc = found
}
if accdoc != nil {
accdoc["code"] = service
delete(accdoc, "uid")
delete(accdoc, "_id")
var bi blockinfo
if err := mg.mongoClient.FindOneAs(CollectionBlock, bson.M{
"code": service,
"accid": accdoc["accid"],
}, &bi); err != nil {
return err
}
if !bi.Start.Time().IsZero() && bi.End.Time().After(time.Now().UTC()) {
accdoc["blocked"] = bi
}
return json.NewEncoder(w).Encode(accdoc)
}
} else if r.Method == "POST" {
var account struct {
Code string
Accid string
Blocked blockinfo
}
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &account); err != nil {
return err
}
accid, _ := primitive.ObjectIDFromHex(account.Accid)
if !account.Blocked.Start.Time().IsZero() && account.Blocked.Start.Time().After(time.Now().UTC()) {
if _, _, err := mg.mongoClient.Update(CollectionBlock, bson.M{
"code": account.Code,
"accid": accid,
}, bson.M{
"$set": account.Blocked,
}, options.Update().SetUpsert(true)); err != nil {
return err
}
}
}
return nil
}
var errApiTokenMissing = errors.New("mg-x-api-token is missing")
func (caller apiCaller) configAPI(w http.ResponseWriter, r *http.Request) error {
mg := caller.mg
apitoken := r.Header.Get("MG-X-API-TOKEN")
if len(apitoken) == 0 {
return errApiTokenMissing
}
if _, exists := mg.apiTokenToService.get(apitoken); !exists {
return fmt.Errorf("mg-x-api-token is not valid : %s", apitoken)
}
return nil
}
var noauth = flag.Bool("noauth", false, "")
type apiCaller struct {
userinfo map[string]any
admins map[string]bool
mg *Maingate
}
func (mg *Maingate) api(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
var userinfo map[string]any
if !*noauth {
authheader := r.Header.Get("Authorization")
if len(authheader) == 0 {
logger.Println("Authorization header is not valid :", authheader)
w.WriteHeader(http.StatusBadRequest)
return
}
req, _ := http.NewRequest("GET", "https://graph.microsoft.com/oidc/userinfo", nil)
req.Header.Add("Authorization", authheader)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Println("graph microsoft api call failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if err = json.Unmarshal(raw, &userinfo); err != nil {
return
}
if _, expired := userinfo["error"]; expired {
w.WriteHeader(http.StatusUnauthorized)
return
}
}
ptr := atomic.LoadPointer(&mg.admins)
adminsptr := (*globalAdmins)(ptr)
if adminsptr.modtime != common.ConfigModTime() {
var config globalAdmins
if err := common.LoadConfig(&config); err == nil {
config.parse()
adminsptr = &config
atomic.StorePointer(&mg.admins, unsafe.Pointer(adminsptr))
}
}
logger.Println("api call :", r.URL.Path, r.Method, r.URL.Query(), userinfo)
caller := apiCaller{
userinfo: userinfo,
admins: adminsptr.emails,
mg: mg,
}
var err error
if strings.HasSuffix(r.URL.Path, "/service") {
err = caller.serviceAPI(w, r)
} else if strings.HasSuffix(r.URL.Path, "/whitelist") {
err = caller.whitelistAPI(w, r)
} else if strings.HasSuffix(r.URL.Path, "/config") {
err = caller.configAPI(w, r)
} else if strings.HasSuffix(r.URL.Path, "/account") {
err = caller.accountAPI(w, r)
}
if err != nil {
logger.Error(err)
}
}

959
core/maingate.go Normal file
View File

@ -0,0 +1,959 @@
package core
import (
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"log"
"math/big"
"math/rand"
"net"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
"repositories.action2quare.com/ayo/go-ayo/common"
"repositories.action2quare.com/ayo/go-ayo/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"
firebase "firebase.google.com/go"
"firebase.google.com/go/auth"
"google.golang.org/api/option"
)
var (
CollectionLink = common.CollectionName("link")
CollectionAuth = common.CollectionName("auth")
CollectionWhitelist = common.CollectionName("whitelist")
CollectionService = common.CollectionName("service")
CollectionBlock = common.CollectionName("block")
CollectionPlatformLoginToken = common.CollectionName("platform_login_token") //-- 각 플랫폼에 로그인 및 권한 받아오는 과정에 사용하는 Key
CollectionUserToken = common.CollectionName("usertoken")
CollectionGamepotUserInfo = common.CollectionName("gamepot_userinfo") //-- 클라로부터 수집된 gamepot 정보 - server to server로 유효성이 검증되진 않았지만 수집은 한다.
CollectionFirebaseUserInfo = common.CollectionName("firebase_userinfo") //-- Firebase UserInfo
)
const (
AuthPlatformFirebaseAuth = "firebase"
AuthPlatformGamepot = "gamepot"
AuthPlatformGoogle = "google"
AuthPlatformMicrosoft = "microsoft"
AuthPlatformApple = "apple"
AuthPlatformTwitter = "twitter"
)
const (
sessionTTL = time.Hour
sessionTTLDev = 1 * time.Hour
)
func SessionTTL() time.Duration {
if *common.Devflag {
return sessionTTLDev
}
return sessionTTL
}
type mongoAuthCell struct {
src *common.Authinfo
}
func init() {
if *common.Devflag {
hostname, _ := os.Hostname()
CollectionLink = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionLink)))
CollectionAuth = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionAuth)))
CollectionWhitelist = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionWhitelist)))
CollectionService = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionService)))
CollectionBlock = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionBlock)))
CollectionPlatformLoginToken = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionPlatformLoginToken)))
CollectionUserToken = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionUserToken)))
CollectionGamepotUserInfo = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionGamepotUserInfo)))
CollectionFirebaseUserInfo = common.CollectionName(fmt.Sprintf("%s-%s", hostname, string(CollectionFirebaseUserInfo)))
}
}
func (ac *mongoAuthCell) ToAuthinfo() *common.Authinfo {
if ac.src == nil {
logger.Error("mongoAuthCell ToAuthinfo failed. ac.src is nil")
}
return ac.src
}
func (ac *mongoAuthCell) ToBytes() []byte {
bt, _ := json.Marshal(ac.src)
return bt
}
func makeAuthCollection(mongoClient common.MongoClient, sessionTTL time.Duration) *common.AuthCollection {
authcoll := common.MakeAuthCollection(sessionTTL)
authcoll.SessionRemoved = func(sk string) {
skid, _ := primitive.ObjectIDFromHex(sk)
mongoClient.Delete(CollectionAuth, bson.M{
"sk": skid,
})
}
authcoll.QuerySession = func(sk string, token string) common.AuthinfoCell {
skid, _ := primitive.ObjectIDFromHex(sk)
var outcell mongoAuthCell
err := mongoClient.FindOneAs(CollectionAuth, bson.M{
"sk": skid,
}, &outcell.src, options.FindOne().SetHint("skonly"))
if err != nil {
logger.Error("QuerySession failed :", err)
return nil
}
if outcell.src == nil {
return nil
}
return &outcell
}
return authcoll
}
type apiTokenMap struct {
sync.Mutex
tokenToService map[string]string
}
func (tm *apiTokenMap) add(token string, serviceCode string) {
tm.Lock()
defer tm.Unlock()
tm.tokenToService[token] = serviceCode
}
func (tm *apiTokenMap) remove(token string) {
tm.Lock()
defer tm.Unlock()
delete(tm.tokenToService, token)
}
func (tm *apiTokenMap) get(token string) (code string, exists bool) {
tm.Lock()
defer tm.Unlock()
code, exists = tm.tokenToService[token]
return
}
type maingateConfig struct {
Mongo string `json:"maingate_mongodb_url"`
SessionTTL int64 `json:"maingate_session_ttl"`
Autologin_ttl int64 `json:"autologin_ttl"`
RedirectBaseUrl string `json:"redirect_base_url"`
GoogleClientId string `json:"google_client_id"`
GoogleClientSecret string `json:"google_client_secret"`
TwitterOAuthKey string `json:"twitter_oauth_key"`
TwitterOAuthSecret string `json:"twitter_oauth_secret"`
TwitterCustomerKey string `json:"twitter_customer_key"`
TwitterCustomerSecret string `json:"twitter_customer_secret"`
AppleCientId string `json:"apple_client_id"`
ApplePrivateKey string `json:"apple_privatekey"`
AppleServiceId string `json:"apple_service_id"`
AppleTeamId string `json:"apple_team_id"`
AppleKeyId string `json:"apple_key_id"`
MicrosoftClientId string `json:"microsoft_client_id"`
MicrosoftClientSecret string `json:"microsoft_client_secret"`
GamepotProjectId string `json:"gamepot_project_id"`
GamepotLoginCheckAPIURL string `json:"gamepot_logincheckapi_url"`
FirebaseAdminSDKCredentialFile string `json:"firebase_admin_sdk_credentialfile"`
}
type globalAdmins struct {
Admins []string `json:"maingate_global_admins"`
emails map[string]bool
modtime time.Time
}
func (ga *globalAdmins) parse() {
parsed := make(map[string]bool)
for _, admin := range ga.Admins {
parsed[admin] = true
}
ga.emails = parsed
ga.modtime = common.ConfigModTime()
}
type servicelist struct {
services unsafe.Pointer
}
func (sl *servicelist) init(total []*serviceDescription) error {
next := make(map[string]*serviceDescription)
for _, service := range total {
next[service.ServiceName] = service
}
atomic.StorePointer(&sl.services, unsafe.Pointer(&next))
return nil
}
func (sl *servicelist) add(s *serviceDescription) {
ptr := atomic.LoadPointer(&sl.services)
src := (*map[string]*serviceDescription)(ptr)
next := map[string]*serviceDescription{}
for k, v := range *src {
next[k] = v
}
next[s.ServiceName] = s
atomic.StorePointer(&sl.services, unsafe.Pointer(&next))
}
func (sl *servicelist) get(sn any) *serviceDescription {
ptr := atomic.LoadPointer(&sl.services)
src := *(*map[string]*serviceDescription)(ptr)
switch sn := sn.(type) {
case string:
return src[sn]
case primitive.ObjectID:
for _, desc := range src {
if desc.Id == sn {
return desc
}
}
}
return nil
}
func (sl *servicelist) all() map[string]*serviceDescription {
ptr := atomic.LoadPointer(&sl.services)
src := (*map[string]*serviceDescription)(ptr)
next := map[string]*serviceDescription{}
for k, v := range *src {
next[k] = v
}
return next
}
func (sl *servicelist) remove(uid primitive.ObjectID) (out *serviceDescription) {
ptr := atomic.LoadPointer(&sl.services)
src := (*map[string]*serviceDescription)(ptr)
next := map[string]*serviceDescription{}
var targetkey string
out = nil
for k, v := range *src {
next[k] = v
if v.Id == uid {
targetkey = k
out = v
}
}
delete(next, targetkey)
atomic.StorePointer(&sl.services, unsafe.Pointer(&next))
return
}
// Maingate :
type Maingate struct {
maingateConfig
mongoClient common.MongoClient
auths *common.AuthCollection
services servicelist
admins unsafe.Pointer
apiTokenToService apiTokenMap
tokenEndpoints map[string]string
authorizationEndpoints map[string]string
userinfoEndpoint map[string]string
jwksUri map[string]string
webTemplate map[string]*template.Template
firebaseAppClient *auth.Client
firebaseAppContext context.Context
}
// New :
func New(ctx context.Context) (*Maingate, error) {
var config maingateConfig
if err := common.LoadConfig(&config); err != nil {
return nil, err
}
var admins globalAdmins
if err := common.LoadConfig(&admins); err == nil {
admins.parse()
}
if config.SessionTTL == 0 {
config.SessionTTL = 3600
}
mg := Maingate{
maingateConfig: config,
services: servicelist{},
admins: unsafe.Pointer(&admins),
apiTokenToService: apiTokenMap{
tokenToService: make(map[string]string),
},
tokenEndpoints: make(map[string]string),
authorizationEndpoints: make(map[string]string),
userinfoEndpoint: make(map[string]string),
jwksUri: make(map[string]string),
}
err := mg.prepare(ctx)
if err != nil {
logger.Error("mg.prepare() failed :", err)
return nil, err
}
opt := option.WithCredentialsFile(mg.FirebaseAdminSDKCredentialFile)
firebaseApp, err := firebase.NewApp(context.Background(), nil, opt)
if err != nil {
logger.Error("firebase admin error initializing app failed :", err)
return nil, err
}
mg.firebaseAppContext = ctx
mg.firebaseAppClient, err = firebaseApp.Auth(mg.firebaseAppContext)
if err != nil {
log.Fatalf("error getting Auth client: %v\n", err)
}
return &mg, nil
}
func (mg *Maingate) Destructor() {
logger.Println("maingate.Destructor")
mg.mongoClient.Close()
}
func (mg *Maingate) discoverOpenIdConfiguration(name string, url string) error {
// microsoft open-id configuration
discover, err := http.Get(url)
if err != nil {
return err
}
defer discover.Body.Close()
body, err := io.ReadAll(discover.Body)
if err != nil {
return err
}
var endpoints map[string]any
if err := json.Unmarshal(body, &endpoints); err != nil {
return err
}
if tokenEndpoint, ok := endpoints["token_endpoint"].(string); ok {
mg.tokenEndpoints[name] = tokenEndpoint
if !ok {
return fmt.Errorf("token_endpoint is missing. %s %s", name, url)
}
}
if authorizationEndpoint, ok := endpoints["authorization_endpoint"].(string); ok {
mg.authorizationEndpoints[name] = authorizationEndpoint
if !ok {
return fmt.Errorf("authorization_endpoint is missing. %s %s", name, url)
}
}
if userinfoEndpoint, ok := endpoints["userinfo_endpoint"].(string); ok {
mg.userinfoEndpoint[name] = userinfoEndpoint
if !ok {
return fmt.Errorf("userinfo_endpoint is missing. %s %s", name, url)
}
}
if jwksUri, ok := endpoints["jwks_uri"].(string); ok {
mg.jwksUri[name] = jwksUri
if !ok {
return fmt.Errorf("jwks_uri is missing. %s %s", name, url)
}
}
return nil
}
func (mg *Maingate) prepare(context context.Context) (err error) {
if err := mg.discoverOpenIdConfiguration(AuthPlatformMicrosoft, "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"); err != nil {
return err
}
if err := mg.discoverOpenIdConfiguration("google", "https://accounts.google.com/.well-known/openid-configuration"); err != nil {
return err
}
mg.webTemplate = make(map[string]*template.Template)
mg.webTemplate[AuthPlatformGamepot] = template.Must(template.ParseFiles("www/gamepot.html"))
// redis에서 env를 가져온 후에
mg.mongoClient, err = common.NewMongoClient(context, mg.Mongo, "maingate")
if err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionAuth, map[string]bson.D{
"skonly": {{Key: "sk", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionLink, map[string]bson.D{
"platformuid": {{Key: "platform", Value: 1}, {Key: "uid", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionLink, map[string]bson.D{
"emailplatform": {{Key: "email", Value: 1}, {Key: "platform", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeIndices(CollectionWhitelist, map[string]bson.D{
"service": {{Key: "service", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeExpireIndex(CollectionWhitelist, 10); err != nil {
return err
}
if err = mg.mongoClient.MakeExpireIndex(CollectionAuth, int32(mg.SessionTTL+300)); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionBlock, map[string]bson.D{
"codeaccid": {{Key: "code", Value: 1}, {Key: "accid", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeExpireIndex(CollectionBlock, int32(3)); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionPlatformLoginToken, map[string]bson.D{
"platformauthtoken": {{Key: "platform", Value: 1}, {Key: "key", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeExpireIndex(CollectionPlatformLoginToken, int32(mg.SessionTTL+300)); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionUserToken, map[string]bson.D{
"platformusertoken": {{Key: "platform", Value: 1}, {Key: "userid", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionGamepotUserInfo, map[string]bson.D{
"gamepotuserid": {{Key: "gamepotuserid", Value: 1}},
}); err != nil {
return err
}
if err = mg.mongoClient.MakeUniqueIndices(CollectionFirebaseUserInfo, map[string]bson.D{
"firebaseuserid": {{Key: "firebaseuserid", Value: 1}},
}); err != nil {
return err
}
mg.auths = makeAuthCollection(mg.mongoClient, time.Duration(mg.SessionTTL*int64(time.Second)))
go watchAuthCollection(context, mg.auths, mg.mongoClient)
go mg.watchWhitelistCollection(context)
return nil
}
func whitelistKey(email string) string {
if strings.HasPrefix(email, "*@") {
// 도메인 전체 허용
return email[2:]
}
return email
}
func (mg *Maingate) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error {
var allServices []*serviceDescription
logger.Println(CollectionService)
if err := mg.mongoClient.FindAllAs(CollectionService, bson.M{}, &allServices, options.Find().SetReturnKey(false)); err != nil {
return err
}
for _, service := range allServices {
if err := service.prepare(mg); err != nil {
return err
}
}
logger.Println("RegisterHandlers...")
mg.services.init(allServices)
for _, service := range allServices {
if service.Closed {
continue
}
logger.Println("ServiceCode:", service.ServiceCode)
serveMux.Handle(common.MakeHttpHandlerPattern(prefix, service.ServiceCode, "/"), service)
}
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "api", "/"), mg.api)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "query", "/"), mg.query)
configraw, _ := json.Marshal(mg.maingateConfig)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "config"), func(w http.ResponseWriter, r *http.Request) {
apitoken := r.Header.Get("MG-X-API-TOKEN")
if len(apitoken) == 0 {
logger.Println("MG-X-API-TOKEN is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
_, exists := mg.apiTokenToService.get(apitoken)
if !exists {
logger.Println("MG-X-API-TOKEN is invalid :", apitoken)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(configraw)
})
fsx := http.FileServer(http.Dir("./console"))
serveMux.Handle(common.MakeHttpHandlerPattern(prefix, "console", "/"), http.StripPrefix("/console/", fsx))
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "request_login_url", AuthPlatformGoogle), mg.platform_google_get_login_url)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize", AuthPlatformGoogle), mg.platform_google_authorize)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize_result", AuthPlatformGoogle), mg.platform_google_authorize_result)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "request_login_url", AuthPlatformMicrosoft), mg.platform_microsoft_get_login_url)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize", AuthPlatformMicrosoft), mg.platform_microsoft_authorize)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize_result", AuthPlatformMicrosoft), mg.platform_microsoft_authorize_result)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "request_login_url", AuthPlatformTwitter), mg.platform_twitter_get_login_url)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize", AuthPlatformTwitter), mg.platform_twitter_authorize)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize_result", AuthPlatformTwitter), mg.platform_twitter_authorize_result)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "request_login_url", AuthPlatformApple), mg.platform_apple_get_login_url)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize", AuthPlatformApple), mg.platform_apple_authorize)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize_result", AuthPlatformApple), mg.platform_apple_authorize_result)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "request_login_url", AuthPlatformGamepot), mg.platform_gamepot_get_login_url)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize", AuthPlatformGamepot), mg.platform_gamepot_authorize)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize_sdk", AuthPlatformGamepot), mg.platform_gamepot_authorize_sdk)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "request_login_url", AuthPlatformFirebaseAuth), mg.platform_firebaseauth_get_login_url)
serveMux.HandleFunc(common.MakeHttpHandlerPattern(prefix, "authorize_sdk", AuthPlatformFirebaseAuth), mg.platform_firebaseauth_authorize_sdk)
go mg.watchServiceCollection(ctx, serveMux, prefix)
// fsx := http.FileServer(http.Dir("console"))
// serveMux.Handle("/console/", http.StripPrefix("/console/", fsx))
// logger.Println("console file server open")
return nil
}
func (mg *Maingate) query(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
queryvals := r.URL.Query()
sk := queryvals.Get("sk")
if len(sk) == 0 {
w.WriteHeader(http.StatusUnauthorized)
return
}
info := mg.auths.Find(sk)
if info == nil {
logger.Println("session key is not valid :", sk)
w.WriteHeader(http.StatusUnauthorized)
return
}
apitoken := r.Header.Get("MG-X-API-TOKEN")
if len(apitoken) == 0 {
logger.Println("MG-X-API-TOKEN is missing")
w.WriteHeader(http.StatusBadRequest)
return
}
servicecode, exists := mg.apiTokenToService.get(apitoken)
if !exists {
logger.Println("MG-X-API-TOKEN is invalid :", apitoken)
w.WriteHeader(http.StatusBadRequest)
return
}
if info.ServiceCode != servicecode {
logger.Println("session is not for this service :", info.ServiceCode, servicecode)
w.WriteHeader(http.StatusBadRequest)
return
}
bt, _ := json.Marshal(info)
w.Write(bt)
}
func (mg *Maingate) GeneratePlatformLoginNonceKey() string {
const allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 52)
for i := range b {
b[i] = allowed[rand.Intn(len(allowed))]
}
return string(b)
}
func (mg *Maingate) GetUserBrowserInfo(r *http.Request) (string, error) {
//=========== 브라우저 검증쪽은 앞으로 빼자
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
logger.Error("Error: RemoteAddr Split Error :", err)
}
cookie, err := r.Cookie("ActionSquareSessionExtraInfo")
if err != nil {
return "", err
}
requestinfo := fmt.Sprintf("%s_%s", cookie.Value, host)
return requestinfo, nil
}
func (mg *Maingate) setUserToken(info usertokeninfo) error {
mg.mongoClient.Delete(CollectionUserToken, bson.M{
"platform": info.platform,
"userid": info.userid,
})
_, _, err := mg.mongoClient.Update(CollectionUserToken, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"platform": info.platform,
"userid": info.userid,
"token": info.token,
"secret": info.secret,
"brinfo": info.brinfo,
"lastupdate": time.Now().Unix(),
"accesstoken": info.accesstoken,
"accesstoken_expire_time": info.accesstoken_expire_time,
},
}, options.Update().SetUpsert(true))
return err
}
func (mg *Maingate) getUserTokenWithCheck(platform string, userid string, brinfo string) (usertokeninfo, error) {
var info usertokeninfo
found, err := mg.mongoClient.FindOne(CollectionUserToken, bson.M{
"platform": platform,
"userid": userid,
"brinfo": brinfo,
})
if err != nil {
return info, err
}
if found == nil {
return info, errors.New("user token not found: " + platform + " / " + userid + " / " + brinfo)
}
updatetime, ok := found["lastupdate"].(int64)
if !ok || time.Now().Unix()-updatetime < mg.maingateConfig.Autologin_ttl {
info.platform = platform
info.userid = userid
info.brinfo = brinfo
token := found["token"]
if token != nil {
info.token = token.(string)
}
secret := found["secret"]
if secret != nil {
info.secret = secret.(string)
}
accesstoken := found["accesstoken"]
if accesstoken != nil {
info.accesstoken = accesstoken.(string)
}
accesstoken_expire_time := found["accesstoken_expire_time"]
if accesstoken_expire_time != nil {
info.accesstoken_expire_time = accesstoken_expire_time.(int64)
}
return info, nil
}
return info, errors.New("session is expired")
}
func (mg *Maingate) updateUserinfo(info usertokeninfo) (bool, string, string) {
var success bool
var userid, email string
success = false
userid = ""
email = ""
switch info.platform {
case AuthPlatformApple:
success, userid, email = mg.platform_apple_getuserinfo(info.token)
case AuthPlatformTwitter:
success, userid, email = mg.platform_twitter_getuserinfo(info.token, info.secret)
case AuthPlatformMicrosoft:
success, userid, email = mg.platform_microsoft_getuserinfo(info)
case AuthPlatformGoogle:
success, userid, email = mg.platform_google_getuserinfo(info)
case AuthPlatformGamepot:
success, userid, email = mg.platform_gamepot_getuserinfo(info)
case AuthPlatformFirebaseAuth:
success, userid, email = mg.platform_firebase_getuserinfo(info)
}
if !success {
return false, "", ""
}
if info.userid != userid {
logger.Error("userinfo / id is not match. :", info.userid, " / ", userid)
return false, "", ""
}
mg.setUserToken(info)
return success, userid, email
}
func (mg *Maingate) getProviderInfo(platform string, uid string) (error, string, string) {
provider := ""
providerid := ""
switch platform {
case AuthPlatformFirebaseAuth: // Fireabase 통해서 로그인 하는 경우, 원래 제공하는 Google 혹은 Apple쪽 ID와 일치 시킨다
found, err := mg.mongoClient.FindOne(CollectionFirebaseUserInfo, bson.M{
"firebaseuserid": uid,
})
if err != nil {
return err, "", ""
}
if found == nil {
return errors.New("firebase info not found: " + uid), "", ""
}
provider = found["firebaseprovider"].(string)
providerid = found["firebaseproviderId"].(string)
if provider == "" || providerid == "" {
return errors.New("getProviderInfo - firebase info not found: " + provider + " / " + providerid), "", ""
}
default:
provider = platform
providerid = uid
}
if provider == "" || providerid == "" {
return errors.New("getProviderInfo - provider info not found: " + provider + " / " + providerid), "", ""
}
return nil, provider, providerid
}
var errKeyNotFound = errors.New("key not found")
func downloadSigningCert(keyurl string, kid string, alg string) (rsa.PublicKey, error) {
var publicKey rsa.PublicKey
resp, err := http.Get(keyurl) // GET 호출
if err != nil {
return rsa.PublicKey{}, err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
// 결과 출력
data, err := io.ReadAll(resp.Body)
if err != nil {
return rsa.PublicKey{}, err
}
var parseddata map[string]interface{}
err = json.Unmarshal([]byte(data), &parseddata)
if err != nil {
return rsa.PublicKey{}, err
}
mapKeys, keysok := parseddata["keys"]
if !keysok {
return rsa.PublicKey{}, errKeyNotFound
}
keylist, keyok := mapKeys.([]interface{})
if !keyok {
return rsa.PublicKey{}, errKeyNotFound
}
for _, key := range keylist {
alg_, alg_ok := key.(map[string]interface{})["alg"]
kty_, kty_ok := key.(map[string]interface{})["kty"]
if !alg_ok && kty_ok {
alg_ = kty_
alg_ok = kty_ok
}
kid_, kid_ok := key.(map[string]interface{})["kid"]
nkey_, nkey_ok := key.(map[string]interface{})["n"]
ekey_, ekey_ok := key.(map[string]interface{})["e"]
if alg_ok && kid_ok && nkey_ok && ekey_ok {
if (alg_.(string) == alg || (alg == "RS256" && alg_ == "RSA")) && kid_.(string) == kid {
decode_nkey_, err := base64.RawURLEncoding.DecodeString(nkey_.(string))
if err != nil {
return rsa.PublicKey{}, errors.New("n key decode fail")
}
if err != nil {
return rsa.PublicKey{}, errors.New("e key decode fail")
}
if ekey_.(string) != "AQAB" && ekey_.(string) != "AAEAAQ" {
return rsa.PublicKey{}, errors.New("e key is not AQAB or AAEAAQ")
}
publicKey = rsa.PublicKey{
N: new(big.Int).SetBytes(decode_nkey_),
E: 65537,
}
}
}
}
return publicKey, nil
}
func JWTparseCode(keyurl string, code string) (string, string, string) {
parts := strings.Split(string(code), ".")
if len(parts) != 3 {
logger.Error("ErrCannotDecode:", len(parts))
return "", "", ""
}
decoded, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
logger.Error("ErrInvalidTokenPart:", string(decoded))
return "", "", ""
}
var parseddata map[string]string
err = json.Unmarshal(decoded, &parseddata)
if err != nil {
panic(err)
}
kid, kidok := parseddata["kid"]
alg, algok := parseddata["alg"]
if !kidok {
logger.Error("ErrorHeader_kid_not_found")
return "", "", ""
}
if !algok {
logger.Error("ErrorHeader_alg_not_found")
return "", "", ""
}
publicKey, err := downloadSigningCert(keyurl, kid, alg)
if err != nil {
logger.Error("ErrorHeader_alg_not_found:", err)
return "", "", ""
}
token, err := jwt.Parse(code, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, errors.New("Unexpected signing method:" + token.Header["alg"].(string))
}
return &publicKey, nil
})
if err != nil {
if err := err.(*jwt.ValidationError); err != nil {
if err.Errors == jwt.ValidationErrorExpired {
logger.Error("ValidationErrorExpired:", err)
return "", "", ""
}
logger.Error("JWT Validation Error:", err)
return "", "", ""
}
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
logger.Error("JWT token.Claims Fail: - MapClaims")
return "", "", ""
}
// for claim := range claims {
// fmt.Println(claim, claims[claim])
// }
nonce_, nonchk := claims["nonce"]
nonce := ""
if nonchk {
nonce = nonce_.(string)
}
email_, emailchk := claims["email"]
email := ""
if emailchk {
email = email_.(string)
}
//--- nonce 체크 필요하다.
return claims["sub"].(string), email, nonce
}

399
core/platformapple.go Normal file
View File

@ -0,0 +1,399 @@
package core
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"io"
"net/http"
"net/url"
"strings"
"time"
"repositories.action2quare.com/ayo/go-ayo/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", 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 != "" {
logger.Error("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)
}

View File

@ -0,0 +1,269 @@
package core
import (
"encoding/json"
"errors"
"log"
"net/http"
"net/url"
"time"
"repositories.action2quare.com/ayo/go-ayo/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
func (mg *Maingate) platform_firebaseauth_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")
withSDK := r.URL.Query().Get("withSDK")
// 무조껀 SDK 있어야됨
if withSDK != "1" {
return
}
//fmt.Println("existid =>", existid)
if existid != "" {
//기존 계정이 있는 경우에는 그 계정 부터 조회한다.
info, err := mg.getUserTokenWithCheck(AuthPlatformFirebaseAuth, existid, browserinfo)
if err == nil {
if info.token != "" {
params := url.Values{}
params.Add("id", existid)
params.Add("authtype", AuthPlatformFirebaseAuth)
if withSDK == "1" {
w.Write([]byte("?" + params.Encode()))
}
return
}
}
}
sessionkey := mg.GeneratePlatformLoginNonceKey()
nonce := mg.GeneratePlatformLoginNonceKey()
mg.mongoClient.Delete(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformFirebaseAuth,
"key": sessionkey,
})
_, _, err = mg.mongoClient.Update(CollectionPlatformLoginToken, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"platform": AuthPlatformFirebaseAuth,
"key": sessionkey,
"nonce": nonce,
"brinfo": browserinfo,
},
}, options.Update().SetUpsert(true))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
logger.Error(err)
return
}
// 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)
if withSDK == "1" {
params := url.Values{}
params.Add("nonce", nonce)
w.Write([]byte("?" + params.Encode()))
}
}
type FirebaseSDKAuthInfo struct {
MemberId string `json:"id"`
Code string `json:"code"`
State string `json:"state"`
Nickname string `json:"nickname"`
Provider string `json:"provider"`
ProviderId string `json:"providerId"`
Email string `json:"email"`
PhotoUrl string `json:"photourl"`
PhoneNumber string `json:"phonenumber"`
}
func (mg *Maingate) platform_firebaseauth_authorize_sdk(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
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
}
var authinfo FirebaseSDKAuthInfo
err = json.NewDecoder(r.Body).Decode(&authinfo)
if err != nil {
logger.Println("authinfo decoding fail:", err)
w.WriteHeader(http.StatusBadRequest)
return
}
bSuccess, Result := mg.platform_firebaseauth_authorize_raw(w, brinfo, authinfo.Code, authinfo.State,
cookie.Value, authinfo.MemberId, authinfo.Nickname, authinfo.Provider, authinfo.ProviderId, authinfo.Email, authinfo.PhotoUrl, authinfo.PhoneNumber)
if bSuccess {
w.Write([]byte("?" + Result))
//http.Redirect(w, r, "actionsquare://login?"+Result, http.StatusSeeOther)
} else {
http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther)
}
}
func (mg *Maingate) platform_firebaseauth_authorize_raw(w http.ResponseWriter, brinfo, code, state, cookieSessionKey, memberId, nickname, provider, providerId, email, photourl, phonenumber string) (bool, string) {
found, err := mg.mongoClient.FindOne(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformFirebaseAuth,
"key": cookieSessionKey,
})
if err != nil {
logger.Println("LoginFlowContext_SessionKey find key :", err)
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if found == nil {
logger.Println("LoginFlowContext_SessionKey not found")
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if cookieSessionKey != found["key"] {
logger.Println("LoginFlowContext_SessionKey key not match")
logger.Println(cookieSessionKey)
logger.Println(found["key"])
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if state != found["nonce"] {
logger.Println("LoginFlowContext_SessionKey nonce not match")
logger.Println(state)
logger.Println(found["nonce"])
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if brinfo != found["brinfo"] { //-- 로그인 시작점과 인증점의 브라우저 혹은 접속지 정보가 다르다?
logger.Println("LoginFlowContext_SessionKey brinfo not match ")
logger.Println(brinfo)
logger.Println(found["brinfo"])
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
_, err = mg.firebaseAppClient.VerifyIDToken(mg.firebaseAppContext, code)
if err != nil {
log.Println("error verifying ID token:", err)
return false, ""
}
// log.Println("Verified ID token: ", token)
// log.Println("Verified ID token: ", token.UID)
acceestoken_expire_time := time.Date(2999, 1, int(time.January), 0, 0, 0, 0, time.UTC).Unix()
if memberId != "" && provider != "" && providerId != "" {
var info usertokeninfo
info.platform = AuthPlatformFirebaseAuth
info.userid = memberId
info.token = code
info.brinfo = brinfo
info.accesstoken = ""
info.accesstoken_expire_time = acceestoken_expire_time
mg.setUserToken(info)
mg.mongoClient.Delete(CollectionFirebaseUserInfo, bson.M{
"firebaseuserid": info.userid,
})
_, _, err := mg.mongoClient.Update(CollectionFirebaseUserInfo, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"firebaseuserid": memberId,
"firebasenickname": nickname,
"firebaseprovider": provider,
"firebaseproviderId": providerId,
"firebaseemail": email,
"firebasephotourl": photourl,
"firebasephonenumber": phonenumber,
"updatetime": time.Now(),
},
}, options.Update().SetUpsert(true))
if err != nil {
logger.Error(err)
}
params := url.Values{}
params.Add("id", memberId)
params.Add("authtype", AuthPlatformFirebaseAuth)
return true, params.Encode()
}
return false, ""
}
func (mg *Maingate) platform_firebase_getuserinfo(info usertokeninfo) (bool, string, string) {
found, err := mg.mongoClient.FindOne(CollectionFirebaseUserInfo, bson.M{
"firebaseuserid": info.userid,
})
if err != nil {
logger.Error(err)
return false, "", ""
}
if found == nil {
logger.Error(errors.New("firebase info not found: " + info.userid))
return false, "", ""
}
_, err = mg.firebaseAppClient.VerifyIDToken(mg.firebaseAppContext, info.token)
if err != nil {
log.Println("error verifying ID token:", err)
return false, "", ""
}
tempEmail := found["firebaseemail"].(string)
return true, info.userid, tempEmail
}

344
core/platformgamepot.go Normal file
View File

@ -0,0 +1,344 @@
package core
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"time"
"repositories.action2quare.com/ayo/go-ayo/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
type GamepotTemplate struct {
RedirectBaseUrl string
State string
}
type Gamepot_LoginValidationResponse struct {
Message string `json:"message"`
Status int `json:"status"`
}
func (mg *Maingate) platform_gamepot_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")
withSDK := r.URL.Query().Get("withSDK")
//fmt.Println("existid =>", existid)
if existid != "" {
//기존 계정이 있는 경우에는 그 계정 부터 조회한다.
info, err := mg.getUserTokenWithCheck(AuthPlatformGamepot, existid, browserinfo)
if err == nil {
if info.token != "" {
params := url.Values{}
params.Add("id", existid)
params.Add("authtype", AuthPlatformGamepot)
if withSDK == "1" {
w.Write([]byte("?" + params.Encode()))
} else {
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
}
return
}
}
}
sessionkey := mg.GeneratePlatformLoginNonceKey()
nonce := mg.GeneratePlatformLoginNonceKey()
mg.mongoClient.Delete(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformGamepot,
"key": sessionkey,
})
_, _, err = mg.mongoClient.Update(CollectionPlatformLoginToken, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"platform": AuthPlatformGamepot,
"key": sessionkey,
"nonce": nonce,
"brinfo": browserinfo,
},
}, options.Update().SetUpsert(true))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
logger.Error(err)
return
}
// 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)
if withSDK == "1" {
params := url.Values{}
params.Add("nonce", nonce)
w.Write([]byte("?" + params.Encode()))
} else {
var templateVar GamepotTemplate
templateVar.RedirectBaseUrl = mg.RedirectBaseUrl
templateVar.State = nonce
mg.webTemplate[AuthPlatformGamepot].Execute(w, templateVar)
}
}
func (mg *Maingate) platform_gamepot_authorize(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
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
}
r.ParseForm()
code := r.Form.Get("code")
state := r.Form.Get("state")
gamepotmemberId := r.Form.Get("id")
gamepotnickname := r.Form.Get("nickname")
gamepotprovider := r.Form.Get("provider")
gamepotproviderId := r.Form.Get("providerId")
// gamepotverify := r.Form.Get("verify")
// gamepotagree := r.Form.Get("agree")
bSuccess, Result := mg.platform_gamepot_authorize_raw(w, brinfo, code, state, cookie.Value, gamepotmemberId, gamepotnickname, gamepotprovider, gamepotproviderId)
if bSuccess {
http.Redirect(w, r, "actionsquare://login?"+Result, http.StatusSeeOther)
} else {
http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther)
}
}
type GamePotSDKAuthInfo struct {
Code string `json:"code"`
State string `json:"state"`
MemberId string `json:"id"`
Nickname string `json:"nickname"`
Provider string `json:"provider"`
ProviderId string `json:"providerId"`
// Verify string `json:"verify"`
// Agree string `json:"agree"`
}
func (mg *Maingate) platform_gamepot_authorize_sdk(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
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
}
var authinfo GamePotSDKAuthInfo
err = json.NewDecoder(r.Body).Decode(&authinfo)
if err != nil {
logger.Println("authinfo decoding fail:", err)
w.WriteHeader(http.StatusBadRequest)
return
}
bSuccess, Result := mg.platform_gamepot_authorize_raw(w, brinfo, authinfo.Code, authinfo.State,
cookie.Value, authinfo.MemberId, authinfo.Nickname, authinfo.Provider, authinfo.ProviderId)
if bSuccess {
w.Write([]byte("?" + Result))
//http.Redirect(w, r, "actionsquare://login?"+Result, http.StatusSeeOther)
} else {
http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther)
}
}
func (mg *Maingate) platform_gamepot_authorize_raw(w http.ResponseWriter, brinfo, code, state, cookieSessionKey, gamepotmemberId, gamepotnickname, gamepotprovider, gamepotproviderId string) (bool, string) {
found, err := mg.mongoClient.FindOne(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformGamepot,
"key": cookieSessionKey,
})
if err != nil {
logger.Println("LoginFlowContext_SessionKey find key :", err)
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if found == nil {
logger.Println("LoginFlowContext_SessionKey not found")
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if cookieSessionKey != found["key"] {
logger.Println("LoginFlowContext_SessionKey key not match")
logger.Println(cookieSessionKey)
logger.Println(found["key"])
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if state != found["nonce"] {
logger.Println("LoginFlowContext_SessionKey nonce not match")
logger.Println(state)
logger.Println(found["nonce"])
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
if brinfo != found["brinfo"] { //-- 로그인 시작점과 인증점의 브라우저 혹은 접속지 정보가 다르다?
logger.Println("LoginFlowContext_SessionKey brinfo not match ")
logger.Println(brinfo)
logger.Println(found["brinfo"])
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
//=================
params := url.Values{}
params.Add("projectId", mg.GamepotProjectId)
params.Add("memberId", gamepotmemberId)
params.Add("token", code)
var respLoginCheck Gamepot_LoginValidationResponse
content := params.Encode()
resp, _ := http.Post(mg.GamepotLoginCheckAPIURL, "application/json", bytes.NewBuffer([]byte(content)))
if resp != nil {
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &respLoginCheck)
} else {
logger.Println("gamepot logincheck fail.")
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
// fmt.Println("==============================")
// fmt.Println("respLoginCheck.Status:", respLoginCheck.Status)
// fmt.Println("respLoginCheck.Message:", respLoginCheck.Message)
// fmt.Println("==============================")
if respLoginCheck.Status != 0 {
logger.Errorf("gamepot login fail:", respLoginCheck.Message)
w.WriteHeader(http.StatusBadRequest)
return false, ""
}
acceestoken_expire_time := time.Date(2999, 1, int(time.January), 0, 0, 0, 0, time.UTC).Unix()
if gamepotmemberId != "" && gamepotprovider != "" && gamepotproviderId != "" {
var info usertokeninfo
info.platform = AuthPlatformGamepot
info.userid = gamepotmemberId
//== memberid 제외하고는 모두 client로 부터 온 값이기 때문에 유효성이 확인된 값이 아니다. 하지만, 참조용으로 사용은 한다.
// 정확한 정보는 gamepotid를 gamepot dashboard에서 조회해서 확인 할 수 밖에 없다.
info.token = gamepotprovider + "-" + gamepotproviderId
info.brinfo = brinfo
info.accesstoken = ""
info.accesstoken_expire_time = acceestoken_expire_time
mg.setUserToken(info)
mg.mongoClient.Delete(CollectionGamepotUserInfo, bson.M{
"gamepotuserid": info.userid,
})
_, _, err := mg.mongoClient.Update(CollectionGamepotUserInfo, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"gamepotuserid": gamepotmemberId,
"gamepotnickname": gamepotnickname,
"gamepotprovider": gamepotprovider,
"gamepotproviderId": gamepotproviderId,
// "gamepotverify": gamepotverify,
// "gamepotagree": gamepotagree,
"updatetime": time.Now(),
},
}, options.Update().SetUpsert(true))
if err != nil {
logger.Error(err)
}
params := url.Values{}
params.Add("id", gamepotmemberId)
params.Add("authtype", AuthPlatformGamepot)
return true, params.Encode()
}
return false, ""
}
func (mg *Maingate) platform_gamepot_getuserinfo(info usertokeninfo) (bool, string, string) {
found, err := mg.mongoClient.FindOne(CollectionGamepotUserInfo, bson.M{
"gamepotuserid": info.userid,
})
if err != nil {
logger.Error(err)
return false, "", ""
}
if found == nil {
logger.Error(errors.New("gamepot info not found: " + info.userid))
return false, "", ""
}
gamepotprovider := found["gamepotprovider"].(string)
gamepotproviderId := found["gamepotproviderId"].(string)
if gamepotprovider+"-"+gamepotproviderId != info.token {
logger.Println("gamepot info not match..") //-- token은 플랫폼종류+플랫폼ID로 구성했는데... 검증할 방법이 없어서 client로 부터 온값을 쓴다. 그래도 유저가 조작하지 않는 이상 일치해야 된다.
logger.Println(info.token)
logger.Println(gamepotprovider + "-" + gamepotproviderId)
return false, "", ""
}
tempEmail := info.userid + "@gamepot" //-- 게임팟은 email을 안줘서 일단 gamepotid기준으로 임시값을 할당한다.
return true, info.userid, tempEmail
}

361
core/platformgoogle.go Normal file
View File

@ -0,0 +1,361 @@
package core
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"time"
"repositories.action2quare.com/ayo/go-ayo/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Google_ValidationResponse struct {
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
}
type Google_UserInfoResponse struct {
Sub string `json:"sub"`
Givenname string `json:"given_name"`
Familyname string `json:"family_name"`
Email string `json:"email"`
Locale string `json:"locale"`
}
func (mg *Maingate) platform_google_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(AuthPlatformGoogle, existid, browserinfo)
if err == nil {
if info.token != "" {
params := url.Values{}
params.Add("id", existid)
params.Add("authtype", AuthPlatformGoogle)
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
return
}
}
}
sessionkey := mg.GeneratePlatformLoginNonceKey()
nonce := mg.GeneratePlatformLoginNonceKey()
mg.mongoClient.Delete(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformGoogle,
"key": sessionkey,
})
_, _, err = mg.mongoClient.Update(CollectionPlatformLoginToken, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"platform": AuthPlatformGoogle,
"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.GoogleClientId)
params.Add("response_type", "code")
params.Add("redirect_uri", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformGoogle)
params.Add("scope", "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email")
params.Add("access_type", "offline")
params.Add("prompt", "consent")
params.Add("state", nonce)
// 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
//fmt.Println(mg.authorizationEndpoints[AuthPlatformGoogle] + params.Encode())
//http.Redirect(w, r, "https://appleid.apple.com/auth/authorize?"+params.Encode(), http.StatusSeeOther)
http.Redirect(w, r, mg.authorizationEndpoints[AuthPlatformGoogle]+"?"+params.Encode(), http.StatusSeeOther)
}
func (mg *Maingate) platform_google_authorize(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
// 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)
// set cookie for storing token
cookie2 := http.Cookie{
Name: "LoginFlowContext_code_state",
Value: state,
Expires: time.Now().Add(1 * time.Minute),
SameSite: http.SameSiteLaxMode,
Secure: true,
Path: "/",
}
http.SetCookie(w, &cookie2)
http.Redirect(w, r, mg.RedirectBaseUrl+"/authorize_result/"+AuthPlatformGoogle, http.StatusSeeOther) //-- 바로 받으니까 쿠키 안와서 한번 더 Redirect 시킨다.
}
func (mg *Maingate) platform_google_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
}
cookiestate, err := r.Cookie("LoginFlowContext_code_state")
if err != nil {
logger.Println("state not found", err)
w.WriteHeader(http.StatusBadRequest)
return
}
code := cookiecode.Value
state := cookiestate.Value
found, err := mg.mongoClient.FindOne(CollectionPlatformLoginToken, bson.M{
"key": cookie.Value,
"platform": AuthPlatformGoogle,
})
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
}
//=================
params := url.Values{}
params.Add("client_id", mg.GoogleClientId)
params.Add("redirect_uri", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformGoogle)
params.Add("client_secret", mg.GoogleClientSecret)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
var respReferesh Google_ValidationResponse
acceestoken_expire_time := time.Now().Unix()
content := params.Encode()
resp, _ := http.Post(mg.tokenEndpoints[AuthPlatformGoogle], "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(content)))
if resp != nil {
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &respReferesh)
} else {
logger.Println("tokenEndpoints fail.")
w.WriteHeader(http.StatusBadRequest)
return
}
// 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("Scope:", respReferesh.Scope)
// fmt.Println("==============================")
if respReferesh.RefreshToken == "" {
logger.Errorf("RefreshToken not found")
w.WriteHeader(http.StatusBadRequest)
return
}
acceestoken_expire_time = acceestoken_expire_time + (int64(respReferesh.ExpiresIn) * 3 / 4) //--- 3/4 이상 지나면 업데이트 한다. ms는 보통 3600초 1시간
userid, email, _ := JWTparseCode(mg.jwksUri[AuthPlatformGoogle], respReferesh.IDToken)
if state == "" || state != found["nonce"] {
logger.Errorf("nonce not match")
return
}
if userid != "" && email != "" {
var info usertokeninfo
info.platform = AuthPlatformGoogle
info.userid = userid
info.token = respReferesh.RefreshToken
info.brinfo = brinfo
info.accesstoken = respReferesh.AccessToken
info.accesstoken_expire_time = acceestoken_expire_time
mg.setUserToken(info)
params := url.Values{}
params.Add("id", userid)
params.Add("authtype", AuthPlatformGoogle)
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
} else {
http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther)
}
}
func (mg *Maingate) platform_google_getuserinfo(info usertokeninfo) (bool, string, string) {
// fmt.Println(info.platform)
// fmt.Println(info.userid)
// fmt.Println(info.accesstoken)
// fmt.Println(time.Now().Unix())
// fmt.Println(info.accesstoken_expire_time)
//-- access token 갱신이 필요하다. -- userinfoEndpoint
if time.Now().Unix() > info.accesstoken_expire_time {
params := url.Values{}
params.Add("client_id", mg.GoogleClientId)
params.Add("redirect_uri", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformGoogle)
params.Add("client_secret", mg.GoogleClientSecret)
params.Add("scope", "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email")
params.Add("refresh_token", info.token)
params.Add("grant_type", "refresh_token")
var respReferesh Google_ValidationResponse
acceestoken_expire_time := time.Now().Unix()
content := params.Encode()
resp, _ := http.Post(mg.tokenEndpoints[AuthPlatformGoogle], "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(content)))
if resp != nil {
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &respReferesh)
// fmt.Println("==== accesstoken 업데이트 =====")
// 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("==============================")
acceestoken_expire_time = acceestoken_expire_time + (int64(respReferesh.ExpiresIn) * 3 / 4) //--- 3/4 이상 지나면 업데이트 한다. ms는 보통 3600초 1시간
userid, email, _ := JWTparseCode(mg.jwksUri[AuthPlatformGoogle], respReferesh.IDToken)
if userid != "" && email != "" && info.userid == userid {
info.token = respReferesh.RefreshToken
info.accesstoken = respReferesh.AccessToken
info.accesstoken_expire_time = acceestoken_expire_time
mg.setUserToken(info) //-- accesstoken 업데이트
} else {
logger.Println("JWTparseCode fail.")
return false, "", ""
}
} else {
logger.Println("tokenEndpoints fail.")
return false, "", ""
}
}
//=================
req, _ := http.NewRequest("GET", mg.userinfoEndpoint[AuthPlatformGoogle], nil)
req.Header.Add("Authorization", "Bearer "+info.accesstoken)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Error("authorize query failed :", err)
return false, "", ""
}
defer resp.Body.Close()
var respUserInfo Google_UserInfoResponse
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &respUserInfo)
// fmt.Println(string(body))
// fmt.Println(respUserInfo.Sub)
// fmt.Println(respUserInfo.Email)
if respUserInfo.Sub != info.userid {
logger.Println("userinfoEndpoint fail.")
return false, "", ""
}
return true, info.userid, respUserInfo.Email //?
}

340
core/platformmicrosoft.go Normal file
View File

@ -0,0 +1,340 @@
package core
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"time"
"repositories.action2quare.com/ayo/go-ayo/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Microsoft_ValidationResponse struct {
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
type Microsoft_UserInfoResponse struct {
Sub string `json:"sub"`
Givenname string `json:"givenname"`
Familyname string `json:"familyname"`
Email string `json:"email"`
Locale string `json:"locale"`
}
func (mg *Maingate) platform_microsoft_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(AuthPlatformMicrosoft, existid, browserinfo)
if err == nil {
if info.token != "" {
params := url.Values{}
params.Add("id", existid)
params.Add("authtype", AuthPlatformMicrosoft)
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
return
}
}
}
sessionkey := mg.GeneratePlatformLoginNonceKey()
nonce := mg.GeneratePlatformLoginNonceKey()
mg.mongoClient.Delete(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformMicrosoft,
"key": sessionkey,
})
_, _, err = mg.mongoClient.Update(CollectionPlatformLoginToken, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"platform": AuthPlatformMicrosoft,
"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.MicrosoftClientId)
params.Add("response_type", "code")
params.Add("redirect_uri", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformMicrosoft)
params.Add("response_mode", "query")
params.Add("scope", "openid offline_access https://graph.microsoft.com/mail.read")
params.Add("nonce", nonce)
// 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
//fmt.Println(mg.authorizationEndpoints[AuthPlatformMicrosoft] + params.Encode())
//http.Redirect(w, r, "https://appleid.apple.com/auth/authorize?"+params.Encode(), http.StatusSeeOther)
http.Redirect(w, r, mg.authorizationEndpoints[AuthPlatformMicrosoft]+"?"+params.Encode(), http.StatusSeeOther)
}
func (mg *Maingate) platform_microsoft_authorize(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
code := r.URL.Query().Get("code")
// 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/"+AuthPlatformMicrosoft, http.StatusSeeOther) //-- 바로 받으니까 쿠키 안와서 한번 더 Redirect 시킨다.
}
func (mg *Maingate) platform_microsoft_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": AuthPlatformMicrosoft,
})
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
}
//=================
params := url.Values{}
params.Add("client_id", mg.MicrosoftClientId)
params.Add("redirect_uri", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformMicrosoft)
params.Add("code", code)
params.Add("scope", "openid offline_access https://graph.microsoft.com/mail.read")
params.Add("grant_type", "authorization_code")
params.Add("client_secret", mg.MicrosoftClientSecret)
var respReferesh Microsoft_ValidationResponse
acceestoken_expire_time := time.Now().Unix()
content := params.Encode()
resp, _ := http.Post(mg.tokenEndpoints[AuthPlatformMicrosoft], "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(content)))
if resp != nil {
body, _ := io.ReadAll(resp.Body)
// w.Write(body)
//json.NewDecoder(resp.Body).Decode(respReferesh)
json.Unmarshal(body, &respReferesh)
} else {
logger.Println("tokenEndpoints fail.")
w.WriteHeader(http.StatusBadRequest)
return
}
// 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("==============================")
acceestoken_expire_time = acceestoken_expire_time + (int64(respReferesh.ExpiresIn) * 3 / 4) //--- 3/4 이상 지나면 업데이트 한다. ms는 보통 3600초 1시간
userid, _, nonce := JWTparseCode(mg.jwksUri[AuthPlatformMicrosoft], respReferesh.IDToken)
if nonce == "" || nonce != found["nonce"] {
logger.Errorf("nonce not match")
return
}
if userid != "" {
var info usertokeninfo
info.platform = AuthPlatformMicrosoft
info.userid = userid
info.token = respReferesh.RefreshToken
info.brinfo = brinfo
info.accesstoken = respReferesh.AccessToken
info.accesstoken_expire_time = acceestoken_expire_time
mg.setUserToken(info)
params := url.Values{}
params.Add("id", userid)
params.Add("authtype", AuthPlatformMicrosoft)
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
} else {
http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther)
}
}
func (mg *Maingate) platform_microsoft_getuserinfo(info usertokeninfo) (bool, string, string) {
// fmt.Println(info.platform)
// fmt.Println(info.userid)
// fmt.Println(info.accesstoken)
// fmt.Println(time.Now().Unix())
// fmt.Println(info.accesstoken_expire_time)
//-- access token 갱신이 필요하다. -- userinfoEndpoint
if time.Now().Unix() > info.accesstoken_expire_time {
params := url.Values{}
params.Add("client_id", mg.MicrosoftClientId)
params.Add("redirect_uri", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformMicrosoft)
params.Add("refresh_token", info.token)
params.Add("scope", "openid offline_access https://graph.microsoft.com/mail.read")
params.Add("grant_type", "refresh_token")
params.Add("client_secret", mg.MicrosoftClientSecret)
var respReferesh Microsoft_ValidationResponse
acceestoken_expire_time := time.Now().Unix()
content := params.Encode()
resp, _ := http.Post(mg.tokenEndpoints[AuthPlatformMicrosoft], "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(content)))
if resp != nil {
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &respReferesh)
// fmt.Println("==== accesstoken 업데이트 =====")
// 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("==============================")
acceestoken_expire_time = acceestoken_expire_time + (int64(respReferesh.ExpiresIn) * 3 / 4) //--- 3/4 이상 지나면 업데이트 한다. ms는 보통 3600초 1시간
userid, _, _ := JWTparseCode(mg.jwksUri[AuthPlatformMicrosoft], respReferesh.IDToken)
if userid != "" && info.userid == userid {
info.token = respReferesh.RefreshToken
info.accesstoken = respReferesh.AccessToken
info.accesstoken_expire_time = acceestoken_expire_time
mg.setUserToken(info) //-- accesstoken 업데이트
} else {
logger.Println("JWTparseCode fail.")
return false, "", ""
}
} else {
logger.Println("tokenEndpoints fail.")
return false, "", ""
}
}
//=================
req, _ := http.NewRequest("GET", mg.userinfoEndpoint[AuthPlatformMicrosoft], nil)
req.Header.Add("Authorization", "Bearer "+info.accesstoken)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Error("authorize query failed :", err)
return false, "", ""
}
defer resp.Body.Close()
var respUserInfo Microsoft_UserInfoResponse
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &respUserInfo)
// fmt.Println(string(body))
// fmt.Println(respUserInfo.Sub)
// fmt.Println(respUserInfo.Email)
if respUserInfo.Sub != info.userid {
logger.Println("userinfoEndpoint fail.")
return false, "", ""
}
return true, info.userid, respUserInfo.Email //?
}

417
core/platformtwitter.go Normal file
View File

@ -0,0 +1,417 @@
package core
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"repositories.action2quare.com/ayo/go-ayo/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
func (mg *Maingate) platform_twitter_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(AuthPlatformTwitter, existid, browserinfo)
if err == nil {
if info.token != "" {
params := url.Values{}
params.Add("id", existid)
params.Add("authtype", AuthPlatformTwitter)
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
return
}
}
}
sessionkey := mg.GeneratePlatformLoginNonceKey()
nonce := mg.GeneratePlatformLoginNonceKey()
auth_token, auth_secret := parse_TwitterOAuthToken(mg.CallTwitterAPI_WithAPPKey("https://api.twitter.com/oauth/request_token", "POST", nonce))
if auth_token == "" || auth_secret == "" {
w.WriteHeader(http.StatusBadRequest)
} else {
mg.mongoClient.Delete(CollectionPlatformLoginToken, bson.M{
"platform": AuthPlatformTwitter,
"key": sessionkey,
})
_, _, err := mg.mongoClient.Update(CollectionPlatformLoginToken, bson.M{
"_id": primitive.NewObjectID(),
}, bson.M{
"$setOnInsert": bson.M{
"platform": AuthPlatformTwitter,
"key": sessionkey,
"token": auth_token,
"secret": auth_secret,
"nonce": nonce,
"brinfo": browserinfo,
},
}, options.Update().SetUpsert(true))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
logger.Error(err)
return
}
}
// 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)
params := url.Values{}
params.Add("oauth_token", auth_token)
http.Redirect(w, r, "https://api.twitter.com/oauth/authorize?"+params.Encode(), http.StatusSeeOther)
}
func (mg *Maingate) platform_twitter_authorize(w http.ResponseWriter, r *http.Request) {
// defer r.Body.Close()
// body, _ := io.ReadAll(r.Body)
// bodyString := string(body)
// oauth_token := r.URL.Query().Get("oauth_token")
// oauth_verifier := r.URL.Query().Get("oauth_verifier")
// fmt.Println("bodyString")
// fmt.Println(bodyString)
// fmt.Println("=======================")
// fmt.Println("Req: %s %s", r.URL.Host, r.URL.Path)
// fmt.Println(oauth_token, oauth_verifier)
// fmt.Println("=======================")
// set cookie for storing token
cookie := http.Cookie{
Name: "LoginFlowContext_code",
Value: r.URL.Query().Encode(),
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/"+AuthPlatformTwitter, http.StatusSeeOther) //-- 바로 받으니까 쿠키 안와서 한번 더 Redirect 시킨다.
}
func (mg *Maingate) platform_twitter_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": AuthPlatformTwitter,
})
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
}
urlvalue, err := url.ParseQuery(code)
if err != nil {
logger.Println("Token parse error")
w.WriteHeader(http.StatusBadRequest)
return
}
//===
oauth_token := urlvalue.Get("oauth_token")
oauth_verifier := urlvalue.Get("oauth_verifier")
if oauth_token == "" || oauth_verifier == "" {
w.WriteHeader(400)
return
}
userid, token, secret := getTwitterAccessToken("https://api.twitter.com/oauth/access_token?oauth_token=" + oauth_token + "&oauth_verifier=" + oauth_verifier)
if userid != "" && token != "" && secret != "" {
var info usertokeninfo
info.platform = AuthPlatformTwitter
info.userid = userid
info.token = token
info.secret = secret
info.brinfo = brinfo
mg.setUserToken(info)
params := url.Values{}
params.Add("id", userid)
params.Add("authtype", AuthPlatformTwitter)
http.Redirect(w, r, "actionsquare://login?"+params.Encode(), http.StatusSeeOther)
} else {
http.Redirect(w, r, "actionsquare://error", http.StatusSeeOther)
}
}
func (mg *Maingate) platform_twitter_getuserinfo(token, secret string) (bool, string, string) {
result := mg.CallTwitterAPI("https://api.twitter.com/2/users/me", "GET", token, secret, mg.GeneratePlatformLoginNonceKey())
var TwitterUserInfo struct {
Data struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
} `json:"data"`
}
err := json.Unmarshal([]byte(result), &TwitterUserInfo)
if err != nil {
logger.Error("twitter userinfo api unmarshal fail :", result)
return false, "", ""
}
// fmt.Println("=====================")
// fmt.Println(result)
// fmt.Println(TwitterUserInfo.Data.Id)
// fmt.Println(TwitterUserInfo.Data.Name)
// fmt.Println(TwitterUserInfo.Data.Username)
// fmt.Println("=====================")
return true, TwitterUserInfo.Data.Id, ""
}
func (mg *Maingate) CallTwitterAPI_WithAPPKey(requesturl, method, nonce string) string {
return mg.CallTwitterAPI(requesturl, method, mg.TwitterOAuthKey, mg.TwitterOAuthSecret, nonce)
}
func (mg *Maingate) CallTwitterAPI(requesturl, method, oauth_token, oauth_secret, nonce string) string {
vals := url.Values{}
if method == "GET" {
splited := strings.Split(requesturl, "?")
if len(splited) > 1 {
parameter := splited[1]
args := strings.Split(parameter, "&")
for _, arg := range args {
tempsplited := strings.Split(arg, "=")
if len(tempsplited) == 2 {
vals.Add(tempsplited[0], tempsplited[1])
}
}
}
}
//vals.Add("oauth_callback", "actionclient://callback")
//vals.Add("oauth_callback", "http://127.0.0.1:7770/auth")
vals.Add("oauth_callback", mg.RedirectBaseUrl+"/authorize/"+AuthPlatformTwitter)
vals.Add("oauth_consumer_key", mg.TwitterCustomerKey)
vals.Add("oauth_token", oauth_token)
vals.Add("oauth_signature_method", "HMAC-SHA1")
vals.Add("oauth_timestamp", strconv.Itoa(int(time.Now().Unix())))
vals.Add("oauth_nonce", nonce)
vals.Add("oauth_version", "1.0")
parameterString := strings.Replace(vals.Encode(), "+", "%20", -1)
signatureBase := strings.ToUpper(method) + "&" + url.QueryEscape(strings.Split(requesturl, "?")[0]) + "&" + url.QueryEscape(parameterString)
signingKey := url.QueryEscape(mg.TwitterCustomerSecret) + "&" + url.QueryEscape(oauth_secret)
signature := calculateTwitterSignature(signatureBase, signingKey)
headerString := "OAuth oauth_callback=\"" + url.QueryEscape(vals.Get("oauth_callback")) + "\", oauth_consumer_key=\"" + url.QueryEscape(vals.Get("oauth_consumer_key")) + "\", oauth_nonce=\"" + url.QueryEscape(vals.Get("oauth_nonce")) +
"\", oauth_signature=\"" + url.QueryEscape(signature) + "\", oauth_signature_method=\"" + url.QueryEscape(vals.Get("oauth_signature_method")) +
"\", oauth_timestamp=\"" + url.QueryEscape(vals.Get("oauth_timestamp")) + "\", oauth_token=\"" + url.QueryEscape(vals.Get("oauth_token")) +
"\", oauth_version=\"" + url.QueryEscape(vals.Get("oauth_version")) + "\""
client := &http.Client{}
req, err := http.NewRequest(method, requesturl, nil)
if err != nil {
panic(err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", "0")
req.Header.Add("Accept-Encoding", "application/json")
req.Header.Add("Host", "api.twitter.com")
req.Header.Add("Accept", "*/*")
req.Header.Add("Authorization", headerString)
resp, err := client.Do(req)
if err != nil {
logger.Error(err)
}
defer resp.Body.Close()
strBody := ""
respBody, err := io.ReadAll(resp.Body)
if err == nil {
strBody = string(respBody)
}
if resp.StatusCode != 200 {
logger.Error("Error: ", resp.StatusCode, err, strBody)
return "error"
}
return strBody
}
func getTwitterAccessToken(requesturl string) (string, string, string) {
client := &http.Client{}
req, err := http.NewRequest("POST", requesturl, nil)
if err != nil {
panic(err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", "0")
req.Header.Add("Accept-Encoding", "application/json")
req.Header.Add("Host", "api.twitter.com")
req.Header.Add("Accept", "*/*")
resp, err := client.Do(req)
if err != nil {
logger.Error(err)
return "", "", ""
}
defer resp.Body.Close()
strBody := ""
respBody, err := io.ReadAll(resp.Body)
if err == nil {
strBody = string(respBody)
}
if resp.StatusCode != 200 {
logger.Error("Error: ", resp.StatusCode, err, strBody)
return "", "", ""
}
params := strings.Split(strBody, "&")
oauth_token := ""
oauth_secret := ""
user_id := ""
//screen_name := ""
for _, param := range params {
parsedStr := strings.Split(param, "=")
if len(parsedStr) == 2 {
if parsedStr[0] == "oauth_token" {
oauth_token = parsedStr[1]
}
if parsedStr[0] == "oauth_token_secret" {
oauth_secret = parsedStr[1]
}
if parsedStr[0] == "user_id" {
user_id = parsedStr[1]
}
// if parsedStr[0] == "screen_name" {
// screen_name = parsedStr[1]
// }
}
}
// ret := ""
// if oauth_token != "" && oauth_secret != "" && user_id != "" && screen_name != "" {
// ret = "{ \"AuthToken\": \"" + oauth_token + "\", \"UserId\": \"" + user_id + "\", \"Screen_name\": \"" + screen_name + "\" }"
// }
// return ret, user_id, oauth_token, oauth_secret
return user_id, oauth_token, oauth_secret
}
func calculateTwitterSignature(base, key string) string {
hash := hmac.New(sha1.New, []byte(key))
hash.Write([]byte(base))
signature := hash.Sum(nil)
return base64.StdEncoding.EncodeToString(signature)
}
func parse_TwitterOAuthToken(strBody string) (string, string) {
params := strings.Split(strBody, "&")
oauth_token := ""
oauth_secret := ""
for _, param := range params {
parsedStr := strings.Split(param, "=")
if len(parsedStr) == 2 {
if parsedStr[0] == "oauth_token" {
oauth_token = parsedStr[1]
}
if parsedStr[0] == "oauth_token_secret" {
oauth_secret = parsedStr[1]
}
}
}
return oauth_token, oauth_secret
}

612
core/service.go Normal file
View File

@ -0,0 +1,612 @@
package core
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
"unsafe"
"repositories.action2quare.com/ayo/go-ayo/logger"
"repositories.action2quare.com/ayo/go-ayo/common"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
type blockinfo struct {
Start primitive.DateTime
End primitive.DateTime `bson:"_ts"`
Reason string
}
type whitelistmember struct {
Service string
Email string
Platform string
Desc string
Expired primitive.DateTime `bson:"_ts,omitempty" json:"_ts,omitempty"`
}
type whitelist struct {
emailptr unsafe.Pointer
working int32
}
type usertokeninfo struct {
platform string
userid string
token string //refreshtoken
secret string
brinfo string
accesstoken string // microsoft only
accesstoken_expire_time int64 // microsoft only
}
func (wl *whitelist) init(total []whitelistmember) {
next := make(map[string]*whitelistmember)
for _, member := range total {
next[whitelistKey(member.Email)] = &member
}
atomic.StorePointer(&wl.emailptr, unsafe.Pointer(&next))
atomic.StoreInt32(&wl.working, 1)
}
func (wl *whitelist) add(m *whitelistmember) {
ptr := atomic.LoadPointer(&wl.emailptr)
src := (*map[string]*whitelistmember)(ptr)
next := map[string]*whitelistmember{}
for k, v := range *src {
next[k] = v
}
next[whitelistKey(m.Email)] = m
atomic.StorePointer(&wl.emailptr, unsafe.Pointer(&next))
}
func (wl *whitelist) remove(email string) {
ptr := atomic.LoadPointer(&wl.emailptr)
src := (*map[string]*whitelistmember)(ptr)
next := make(map[string]*whitelistmember)
for k, v := range *src {
next[k] = v
}
delete(next, whitelistKey(email))
atomic.StorePointer(&wl.emailptr, unsafe.Pointer(&next))
}
func (wl *whitelist) isMember(email string, platform string) bool {
if atomic.LoadInt32(&wl.working) == 0 {
return true
}
ptr := atomic.LoadPointer(&wl.emailptr)
src := *(*map[string]*whitelistmember)(ptr)
if member, exists := src[whitelistKey(email)]; exists {
return member.Platform == platform
}
return false
}
type serviceDescription struct {
// sync.Mutex
Id primitive.ObjectID `bson:"_id"`
ServiceName string `bson:"service"`
Divisions map[string]any `bson:"divisions"`
ServiceCode string `bson:"code"`
UseWhitelist bool `bson:"use_whitelist"`
Closed bool `bson:"closed"`
ServerApiTokens []primitive.ObjectID `bson:"api_tokens"`
ApiUsers map[string][]string `bson:"api_users"`
auths *common.AuthCollection
wl whitelist
mongoClient common.MongoClient
sessionTTL time.Duration
closed int32
serviceCodeBytes []byte
getUserBrowserInfo func(r *http.Request) (string, error)
getUserTokenWithCheck func(platform string, userid string, brinfo string) (usertokeninfo, error)
updateUserinfo func(info usertokeninfo) (bool, string, string)
getProviderInfo func(platform string, uid string) (error, string, string)
apiUsers unsafe.Pointer
divisionsSerialized unsafe.Pointer
serviceSerialized unsafe.Pointer
}
func (sh *serviceDescription) readProfile(authtype string, id string, binfo string) (email string, err error) {
defer func() {
s := recover()
if s != nil {
logger.Error("readProfile failed :", authtype, id, s)
if errt, ok := s.(error); ok {
err = errt
} else {
err = errors.New(fmt.Sprint(s))
}
}
}()
userinfo, err := sh.getUserTokenWithCheck(authtype, id, binfo)
if err != nil {
return "", err
}
if userinfo.token == "" {
return "", errors.New("refreshtoken token not found")
}
//-- 토큰으로 모두 확인이 끝났으면 갱신한다.
ok, _, email := sh.updateUserinfo(userinfo)
if !ok {
return "", errors.New("updateUserinfo failed")
}
return email, nil
}
func (sh *serviceDescription) prepare(mg *Maingate) error {
div := sh.Divisions
if len(sh.ServiceCode) == 0 {
sh.ServiceCode = hex.EncodeToString(sh.Id[6:])
}
divmarshaled, _ := json.Marshal(div)
devstr := string(divmarshaled)
sh.divisionsSerialized = unsafe.Pointer(&devstr)
sh.mongoClient = mg.mongoClient
sh.auths = mg.auths
sh.sessionTTL = time.Duration(mg.SessionTTL * int64(time.Second))
sh.wl = whitelist{}
sh.serviceCodeBytes, _ = hex.DecodeString(sh.ServiceCode)
sh.getUserBrowserInfo = mg.GetUserBrowserInfo
sh.getUserTokenWithCheck = mg.getUserTokenWithCheck
sh.updateUserinfo = mg.updateUserinfo
sh.getProviderInfo = mg.getProviderInfo
if sh.Closed {
sh.closed = 1
} else {
sh.closed = 0
}
if sh.UseWhitelist {
var whites []whitelistmember
if err := mg.mongoClient.FindAllAs(CollectionWhitelist, bson.M{
"$or": []bson.M{{"service": sh.ServiceName}, {"service": sh.ServiceCode}},
}, &whites, options.Find().SetReturnKey(false)); err != nil {
return err
}
sh.wl.init(whites)
} else {
sh.wl.working = 0
}
if len(sh.ApiUsers) == 0 {
sh.ApiUsers = map[string][]string{
"service": {},
"whitelist": {},
"account": {},
}
}
parsedUsers := make(map[string]map[string]bool)
for cat, users := range sh.ApiUsers {
catusers := make(map[string]bool)
for _, user := range users {
catusers[user] = true
}
parsedUsers[cat] = catusers
}
sh.apiUsers = unsafe.Pointer(&parsedUsers)
for _, keyid := range sh.ServerApiTokens {
mg.apiTokenToService.add(keyid.Hex(), sh.ServiceCode)
}
bt, _ := json.Marshal(sh)
atomic.StorePointer(&sh.serviceSerialized, unsafe.Pointer(&bt))
logger.Println("service is ready :", sh.ServiceName, sh.ServiceCode, sh.UseWhitelist, string(divmarshaled))
return nil
}
func (sh *serviceDescription) link(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return
}
queryvals := r.URL.Query()
//oldToken := queryvals.Get("otoken")
oldType := queryvals.Get("otype")
oldId := queryvals.Get("oid")
sk := queryvals.Get("sk")
//newToken := queryvals.Get("ntoken")
newType := queryvals.Get("ntype")
newId := queryvals.Get("nid")
oldAuth := sh.auths.Find(sk)
if oldAuth == nil {
// 잘못된 세션
logger.Println("link failed. session key is not valid :", sk)
w.WriteHeader(http.StatusBadRequest)
return
}
// fmt.Println("=================")
// fmt.Println(oldType)
// fmt.Println(oldId)
// fmt.Println("=================")
// fmt.Println(newType)
// fmt.Println(newId)
// fmt.Println("=================")
// fmt.Println(oldAuth.Platform)
// fmt.Println(oldAuth.Uid)
// fmt.Println("=================")
//if oldAuth.Token != oldToken || oldAuth.Uid != oldId || oldAuth.Platform != oldType {
if oldAuth.Uid != oldId || oldAuth.Platform != oldType {
logger.Println("link failed. session key is not correct :", *oldAuth, queryvals)
w.WriteHeader(http.StatusBadRequest)
return
}
bfinfo, err := sh.getUserBrowserInfo(r)
if err != nil {
logger.Error("getUserBrowserInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
_, err = sh.readProfile(oldType, oldId, bfinfo)
if err != nil {
logger.Error("readProfile(old) failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
email, err := sh.readProfile(newType, newId, bfinfo)
if err != nil {
logger.Error("readProfile(new) failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// if len(email) == 0 {
// logger.Println("link failed. email is missing :", r.URL.Query())
// w.WriteHeader(http.StatusBadRequest)
// return
// }
if !sh.wl.isMember(email, newType) {
logger.Println("link failed. not whitelist member :", r.URL.Query(), email)
w.WriteHeader(http.StatusBadRequest)
return
}
err, newType, newId = sh.getProviderInfo(newType, newId)
if err != nil {
logger.Error("getProviderInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
}
createtime := primitive.NewDateTimeFromTime(time.Now().UTC())
link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{
"platform": newType,
"uid": newId,
}, bson.M{
"$setOnInsert": bson.M{
"create": createtime,
"email": email,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1}))
if err != nil {
logger.Error("link failed. FindOneAndUpdate link err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, newid, err := sh.mongoClient.Update(common.CollectionName(sh.ServiceName), bson.M{
"_id": link["_id"].(primitive.ObjectID),
}, bson.M{
"$setOnInsert": bson.M{
"accid": oldAuth.Accid,
"create": createtime,
},
}, options.Update().SetUpsert(true))
if err != nil {
logger.Error("link failed. Update ServiceName err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// newid가 있어야 한다. 그래야 기존 서비스 계정이 없는 상태이다.
if newid == nil {
// 이미 계정이 있네?
logger.Println("link failed. already have service account :", r.URL.Query())
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Println("link success :", r.URL.Query())
}
func (sh *serviceDescription) isValidAPIUser(category string, email string) bool {
ptr := atomic.LoadPointer(&sh.apiUsers)
catusers := *(*map[string]map[string]bool)(ptr)
if category == "*" {
for _, users := range catusers {
if _, ok := users[email]; ok {
return true
}
}
} else if users, ok := catusers[category]; ok {
if _, ok := users[email]; ok {
return true
}
logger.Println("isValidAPIUser failed. email is not allowed :", category, email, users)
}
logger.Println("isValidAPIUser failed. category is missing :", category)
return false
}
func (sh *serviceDescription) authorize(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return
}
queryvals := r.URL.Query()
authtype := queryvals.Get("type")
uid := queryvals.Get("id")
//accesstoken := queryvals.Get("token") //-- 이거 이제 받지마라
session := queryvals.Get("sk")
//email, err := sh.readProfile(authtype, uid, accesstoken)
bfinfo, err := sh.getUserBrowserInfo(r)
if err != nil {
logger.Error("getUserBrowserInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
email, err := sh.readProfile(authtype, uid, bfinfo)
if err != nil {
logger.Error("readProfile failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if !sh.wl.isMember(email, authtype) {
logger.Println("auth failed. not whitelist member :", sh.ServiceCode, authtype, uid, email)
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Println("auth success :", authtype, uid, email, session)
err, newType, newId := sh.getProviderInfo(authtype, uid)
if err != nil {
logger.Error("getProviderInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
}
if authtype != newType || uid != newId {
authtype = newType
uid = newId
logger.Println("auth success ( redirect ) :", authtype, uid, email, session)
}
//if len(session) == 0 && len(email) > 0 {
if len(session) == 0 {
// platform + id -> account id
createtime := primitive.NewDateTimeFromTime(time.Now().UTC())
link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{
"platform": authtype,
"uid": uid,
}, bson.M{
"$setOnInsert": bson.M{
"create": createtime,
"email": email,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1}))
if err != nil {
logger.Error("authorize failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
linkid := link["_id"].(primitive.ObjectID)
newaccid := primitive.NewObjectID()
for i := 0; i < len(sh.serviceCodeBytes); i++ {
newaccid[i] ^= sh.serviceCodeBytes[i]
}
account, err := sh.mongoClient.FindOneAndUpdate(common.CollectionName(sh.ServiceName), bson.M{
"_id": linkid,
}, bson.M{
"$setOnInsert": bson.M{
"accid": newaccid,
"create": createtime,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"accid": 1, "create": 1}))
if err != nil {
logger.Error("authorize failed. Update sh.ServiceName err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
accid := account["accid"].(primitive.ObjectID)
oldcreate := account["create"].(primitive.DateTime)
newaccount := oldcreate == createtime
var bi blockinfo
if err := sh.mongoClient.FindOneAs(CollectionBlock, bson.M{
"code": sh.ServiceCode,
"accid": accid,
}, &bi); err != nil {
logger.Error("authorize failed. find blockinfo in CollectionBlock err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if !bi.Start.Time().IsZero() {
now := time.Now().UTC()
if bi.Start.Time().Before(now) && bi.End.Time().After(now) {
// block됐네?
// status는 정상이고 reason을 넘겨주자
json.NewEncoder(w).Encode(map[string]any{
"blocked": bi,
})
return
}
}
newsession := primitive.NewObjectID()
expired := primitive.NewDateTimeFromTime(time.Now().UTC().Add(sh.sessionTTL))
newauth := common.Authinfo{
Accid: accid,
ServiceCode: sh.ServiceCode,
Platform: authtype,
Uid: uid,
//Token: accesstoken,
Sk: newsession,
Expired: expired,
//RefreshToken: queryvals.Get("rt"),
}
_, _, err = sh.mongoClient.UpsertOne(CollectionAuth, bson.M{"_id": newauth.Accid}, &newauth)
if err != nil {
logger.Error("authorize failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
output := map[string]any{
"sk": newsession.Hex(),
"expirein": sh.sessionTTL.Seconds(),
"newAccount": newaccount,
"accid": newauth.Accid.Hex(),
}
bt, _ := json.Marshal(output)
w.Write(bt)
} else if len(session) > 0 {
sessionobj, _ := primitive.ObjectIDFromHex(session)
if !sessionobj.IsZero() {
updated, _, err := sh.mongoClient.Update(CollectionAuth,
bson.M{
"sk": sessionobj,
},
bson.M{
"$currentDate": bson.M{
"_ts": bson.M{"$type": "date"},
},
}, options.Update().SetUpsert(false))
if err != nil {
logger.Error("update auth collection failed")
logger.Error(err)
return
}
if !updated {
// 세션이 없네?
logger.Println("authorize failed. session not exists in database :", session)
w.WriteHeader(http.StatusUnauthorized)
return
}
output := map[string]any{
"sk": session,
"expirein": sh.sessionTTL.Seconds(),
}
bt, _ := json.Marshal(output)
w.Write(bt)
} else {
logger.Println("authorize failed. sk is not valid hex :", session)
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
logger.Println("authorize failed. id empty :", queryvals)
}
}
func (sh *serviceDescription) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
if atomic.LoadInt32(&sh.closed) != 0 {
w.WriteHeader(http.StatusNotFound)
return
}
if strings.HasSuffix(r.URL.Path, "/auth") {
sh.authorize(w, r)
} else if strings.HasSuffix(r.URL.Path, "/link") {
sh.link(w, r)
} else {
// TODO : 세션키와 authtoken을 헤더로 받아서 accid 조회
queryvals := r.URL.Query()
//token := queryvals.Get("token")
token := "" // 더이상 쓰지 않는다.
sk := queryvals.Get("sk")
//if len(token) == 0 || len(sk) == 0 {
if len(sk) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO : 각 서버에 있는 자산? 캐릭터 정보를 보여줘야 하나. 뭘 보여줄지는 프로젝트에 문의
// 일단 서버 종류만 내려보내자
// 세션키가 있는지 확인
if _, ok := sh.auths.IsValid(sk, token); !ok {
logger.Println("sessionkey is not valid :", sk, token)
w.WriteHeader(http.StatusBadRequest)
return
}
divstrptr := atomic.LoadPointer(&sh.divisionsSerialized)
divstr := *(*string)(divstrptr)
w.Write([]byte(divstr))
}
}

330
core/watch.go Normal file
View File

@ -0,0 +1,330 @@
package core
import (
"context"
"net/http"
"sync/atomic"
"time"
"repositories.action2quare.com/ayo/go-ayo/logger"
"repositories.action2quare.com/ayo/go-ayo/common"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type authPipelineDocument struct {
OperationType string `bson:"operationType"`
DocumentKey struct {
Id primitive.ObjectID `bson:"_id"`
} `bson:"documentKey"`
Authinfo *common.Authinfo `bson:"fullDocument"`
}
type servicePipelineDocument struct {
OperationType string `bson:"operationType"`
DocumentKey struct {
Id primitive.ObjectID `bson:"_id"`
} `bson:"documentKey"`
Service *serviceDescription `bson:"fullDocument"`
}
type whilelistPipelineDocument struct {
OperationType string `bson:"operationType"`
DocumentKey struct {
Id primitive.ObjectID `bson:"_id"`
} `bson:"documentKey"`
Member *whitelistmember `bson:"fullDocument"`
}
func (mg *Maingate) watchWhitelistCollection(parentctx context.Context) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
matchStage := bson.D{
{
Key: "$match", Value: bson.D{
{Key: "operationType", Value: bson.D{
{Key: "$in", Value: bson.A{
"update",
"insert",
}},
}},
},
}}
projectStage := bson.D{
{
Key: "$project", Value: bson.D{
{Key: "documentKey", Value: 1},
{Key: "operationType", Value: 1},
{Key: "fullDocument", Value: 1},
},
},
}
var stream *mongo.ChangeStream
var err error
var ctx context.Context
for {
if stream == nil {
stream, err = mg.mongoClient.Watch(CollectionWhitelist, mongo.Pipeline{matchStage, projectStage})
if err != nil {
logger.Error("watchWhitelistCollection watch failed :", err)
time.Sleep(time.Minute)
continue
}
ctx = context.TODO()
}
changed := stream.TryNext(ctx)
if ctx.Err() != nil {
logger.Error("watchServiceCollection stream.TryNext failed. process should be restarted! :", ctx.Err().Error())
break
}
if changed {
var data whilelistPipelineDocument
if err := stream.Decode(&data); err == nil {
ot := data.OperationType
switch ot {
case "insert":
// 새 화이트리스트 멤버
if svc := mg.services.get(data.Member.Service); svc != nil {
svc.wl.add(data.Member)
}
case "update":
if svc := mg.services.get(data.Member.Service); svc != nil {
if data.Member.Expired != 0 {
logger.Println("whitelist member is removed :", *data.Member)
svc.wl.remove(data.Member.Email)
} else {
logger.Println("whitelist member is updated :", *data.Member)
svc.wl.add(data.Member)
}
}
}
} else {
logger.Error("watchServiceCollection stream.Decode failed :", err)
}
} else if stream.Err() != nil || stream.ID() == 0 {
logger.Error("watchServiceCollection stream error :", stream.Err())
stream.Close(ctx)
stream = nil
} else {
time.Sleep(time.Second)
}
}
}
func (mg *Maingate) watchServiceCollection(parentctx context.Context, serveMux *http.ServeMux, prefix string) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
matchStage := bson.D{
{
Key: "$match", Value: bson.D{
{Key: "operationType", Value: bson.D{
{Key: "$in", Value: bson.A{
"delete",
"insert",
"update",
"replace",
}},
}},
},
}}
projectStage := bson.D{
{
Key: "$project", Value: bson.D{
{Key: "operationType", Value: 1},
{Key: "fullDocument", Value: 1},
},
},
}
var stream *mongo.ChangeStream
var err error
var ctx context.Context
for {
if stream == nil {
stream, err = mg.mongoClient.Watch(CollectionService, mongo.Pipeline{matchStage, projectStage}, options.ChangeStream().SetFullDocument(options.UpdateLookup))
if err != nil {
logger.Error("watchServiceCollection watch failed :", err)
time.Sleep(time.Minute)
continue
}
ctx = context.TODO()
}
changed := stream.TryNext(ctx)
if ctx.Err() != nil {
logger.Error("watchServiceCollection stream.TryNext failed. process should be restarted! :", ctx.Err().Error())
break
}
if changed {
var data servicePipelineDocument
if err := stream.Decode(&data); err == nil {
ot := data.OperationType
switch ot {
case "insert":
// 새 서비스 추가됨
if err := data.Service.prepare(mg); err != nil {
logger.Error("service cannot be prepared :", data.Service, err)
} else {
logger.Println("service is on the board! :", data.Service)
mg.services.add(data.Service)
serveMux.Handle(common.MakeHttpHandlerPattern(prefix, data.Service.ServiceCode, "/"), data.Service)
}
case "replace":
fallthrough
case "update":
data.Service.prepare(mg)
if old := mg.services.get(data.Service.ServiceName); old != nil {
logger.Printf("service is changed : %v", data.Service)
atomic.SwapPointer(&old.divisionsSerialized, data.Service.divisionsSerialized)
atomic.SwapPointer(&old.apiUsers, data.Service.apiUsers)
atomic.SwapPointer(&old.serviceSerialized, data.Service.serviceSerialized)
for _, token := range old.ServerApiTokens {
mg.apiTokenToService.remove(token.Hex())
}
for _, token := range data.Service.ServerApiTokens {
mg.apiTokenToService.add(token.Hex(), data.Service.ServiceCode)
}
if data.Service.UseWhitelist {
atomic.StoreInt32(&old.wl.working, 1)
} else {
atomic.StoreInt32(&old.wl.working, 0)
}
old.Closed = data.Service.Closed
if old.Closed {
atomic.StoreInt32(&old.closed, 1)
} else {
atomic.StoreInt32(&old.closed, 0)
}
atomic.SwapPointer(&old.wl.emailptr, data.Service.wl.emailptr)
old.Divisions = data.Service.Divisions
} else if !data.Service.Closed {
if err := data.Service.prepare(mg); err != nil {
logger.Error("service cannot be prepared :", data.Service, err)
} else {
logger.Println("service is on the board! :", data.Service)
mg.services.add(data.Service)
serveMux.Handle(common.MakeHttpHandlerPattern(prefix, data.Service.ServiceCode, "/"), data.Service)
}
}
case "delete":
if deleted := mg.services.remove(data.DocumentKey.Id); deleted != nil {
logger.Println("service is closed :", data.Service)
atomic.AddInt32(&deleted.closed, 1)
}
}
} else {
logger.Error("watchServiceCollection stream.Decode failed :", err)
}
} else if stream.Err() != nil || stream.ID() == 0 {
logger.Error("watchServiceCollection stream error :", stream.Err())
stream.Close(ctx)
stream = nil
} else {
time.Sleep(time.Second)
}
}
}
func watchAuthCollection(parentctx context.Context, ac *common.AuthCollection, mongoClient common.MongoClient) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
matchStage := bson.D{
{
Key: "$match", Value: bson.D{
{Key: "operationType", Value: bson.D{
{Key: "$in", Value: bson.A{
"delete",
"insert",
"update",
}},
}},
},
}}
projectStage := bson.D{
{
Key: "$project", Value: bson.D{
{Key: "documentKey", Value: 1},
{Key: "operationType", Value: 1},
{Key: "fullDocument", Value: 1},
},
},
}
var stream *mongo.ChangeStream
var err error
var ctx context.Context
for {
if stream == nil {
stream, err = mongoClient.Watch(CollectionAuth, mongo.Pipeline{matchStage, projectStage})
if err != nil {
logger.Error("watchAuthCollection watch failed :", err)
time.Sleep(time.Minute)
continue
}
ctx = context.TODO()
}
changed := stream.TryNext(ctx)
if ctx.Err() != nil {
logger.Error("watchAuthCollection stream.TryNext failed. process should be restarted! :", ctx.Err().Error())
break
}
if changed {
var data authPipelineDocument
if err := stream.Decode(&data); err == nil {
ot := data.OperationType
switch ot {
case "insert":
ac.AddRaw(&mongoAuthCell{src: data.Authinfo})
case "update":
ac.AddRaw(&mongoAuthCell{src: data.Authinfo})
case "delete":
ac.RemoveByAccId(data.DocumentKey.Id)
}
} else {
logger.Error("watchAuthCollection stream.Decode failed :", err)
}
} else if stream.Err() != nil || stream.ID() == 0 {
logger.Error("watchAuthCollection stream error :", stream.Err())
stream.Close(ctx)
stream = nil
} else {
time.Sleep(time.Second)
}
}
}