200 lines
4.4 KiB
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
|
|
|
|
}
|