Files
gocommon/azure/func.go

200 lines
4.4 KiB
Go

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
}