package azure import ( "crypto/rsa" "encoding/base64" "encoding/binary" "encoding/json" "errors" "io" "math" "math/big" "strconv" "net/http" "net/url" "os" "strings" "sync" "time" "github.com/golang-jwt/jwt" ) type accessToken struct { sync.Mutex typetoken string expireAt time.Time url string values url.Values } var jwkCache struct { headerlock sync.Mutex typetoken string expireAt time.Time pks map[string]*rsa.PublicKey } func microsoftAppId() string { val := os.Getenv("MICROSOFT_APP_ID") if len(val) == 0 { val = "b5367590-5a94-4df3-bca0-ecd4b693ddf0" } return val } func microsoftAppPassword() string { val := os.Getenv("MICROSOFT_APP_PASSWORD") if len(val) == 0 { val = "~VG1cf2-~5Fw3Wz9_4.A.XxpZPO8BwJ36y" } return val } func getOpenIDConfiguration(x5t string) (*rsa.PublicKey, error) { // https://docs.microsoft.com/ko-kr/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0 jwkCache.headerlock.Lock() defer jwkCache.headerlock.Unlock() if time.Now().After(jwkCache.expireAt) { resp, err := http.Get("https://login.botframework.com/v1/.well-known/openidconfiguration") if err != nil { return nil, err } defer resp.Body.Close() bt, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var doc map[string]interface{} if err = json.Unmarshal(bt, &doc); err != nil { return nil, err } url := doc["jwks_uri"].(string) resp, err = http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() bt, err = io.ReadAll(resp.Body) if err != nil { return nil, err } if err = json.Unmarshal(bt, &doc); err != nil { return nil, err } keys := doc["keys"].([]interface{}) newPks := make(map[string]*rsa.PublicKey) for _, key := range keys { keydoc := key.(map[string]interface{}) x5t := keydoc["x5t"].(string) eb := make([]byte, 4) nb, _ := base64.RawURLEncoding.DecodeString(keydoc["n"].(string)) base64.RawURLEncoding.Decode(eb, []byte(keydoc["e"].(string))) n := big.NewInt(0).SetBytes(nb) e := binary.LittleEndian.Uint32(eb) pk := &rsa.PublicKey{ N: n, E: int(e), } newPks[x5t] = pk } jwkCache.expireAt = time.Now().Add(24 * time.Hour) jwkCache.pks = newPks } return jwkCache.pks[x5t], nil } func VerifyJWT(header string) error { if !strings.HasPrefix(header, "Bearer ") { return errors.New("invalid token") } tokenString := header[7:] token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return getOpenIDConfiguration(token.Header["x5t"].(string)) }) if err != nil { return err } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { if claims["iss"].(string) != "https://api.botframework.com" { return errors.New("issuer is not valid") } if claims["aud"].(string) != microsoftAppId() { return errors.New("audience is not valid") } expireAt := int64(claims["exp"].(float64)) if math.Abs(float64((expireAt-time.Now().UTC().Unix())/int64(time.Second))) >= 300 { return errors.New("token expired") } } else { return errors.New("VerifyJWT token claims failed") } return nil } func (at *accessToken) getAuthoizationToken() (string, error) { at.Lock() defer at.Unlock() if len(at.url) == 0 { at.url = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" at.values = url.Values{ "grant_type": {"client_credentials"}, "client_id": {microsoftAppId()}, "scope": {"https://api.botframework.com/.default"}, "client_secret": {microsoftAppPassword()}, } } if time.Now().After(at.expireAt) { resp, err := http.PostForm(at.url, at.values) if err != nil { return "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var doc map[string]interface{} err = json.Unmarshal(body, &doc) if err != nil { return "", err } if v, ok := doc["error"]; ok { if desc, ok := doc["error_description"]; ok { return "", errors.New(desc.(string)) } return "", errors.New(v.(string)) } tokenType := doc["token_type"].(string) token := doc["access_token"].(string) expin := doc["expires_in"] var tokenDur int switch expin := expin.(type) { case float64: tokenDur = int(expin) case string: tokenDur, _ = strconv.Atoi(expin) } at.typetoken = tokenType + " " + token at.expireAt = time.Now().Add(time.Duration(tokenDur) * time.Second) } return at.typetoken, nil }