Files
apinto/drivers/plugins/oauth2-introspection/introspect.go
2025-05-14 17:31:09 +08:00

156 lines
4.0 KiB
Go

package oauth2_introspection
import (
"encoding/json"
"fmt"
"github.com/eolinker/eosc"
http_service "github.com/eolinker/eosc/eocontext/http-context"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type IntrospectionResponseBody struct {
Active bool `json:"active"`
ClientId string `json:"client_id"`
Username string `json:"username"`
Scope string `json:"scope"`
Sub string `json:"sub"`
Aud string `json:"aud"`
Iss string `json:"iss"`
Exp int64 `json:"exp"`
Iat int64 `json:"iat"`
Nbf int64 `json:"nbf"`
Jti string `json:"jti"`
}
func setAppLabel(ctx http_service.IHttpContext, t *IntrospectionResponseBody, consumerBy string, allowAnonymous bool) error {
consumer := t.ClientId
switch consumerBy {
case "client_id":
case "username":
consumer = t.Username
default:
return fmt.Errorf("invalid consumer_by")
}
a, has := appManager.GetApp(consumer)
if !has {
if !allowAnonymous {
return fmt.Errorf("consumer(%s) not found", consumer)
}
a = appManager.AnonymousApp()
if a == nil {
return fmt.Errorf("anonymous app not found")
}
ctx.Proxy().Header().SetHeader("X-Consumer-Anonymous", "true")
}
ctx.SetLabel("application_id", a.Id())
ctx.SetLabel("application_name", a.Name())
ctx.Proxy().Header().SetHeader("X-Consumer-ID", a.Id())
ctx.Proxy().Header().SetHeader("X-Consumer-Username", a.Name())
return nil
}
func verifyIntrospection(t *IntrospectionResponseBody, clientId string, scopes map[string]struct{}) error {
if t.Active != true {
return fmt.Errorf("token is not active")
}
if t.ClientId != clientId {
return fmt.Errorf("invalid client_id")
}
now := time.Now()
if t.Exp < now.Unix() {
return fmt.Errorf("token is expired")
}
if t.Iat > now.Unix() {
return fmt.Errorf("token is not yet active")
}
if len(scopes) > 0 {
if _, ok := scopes[t.Scope]; !ok {
return fmt.Errorf("invalid scope")
}
}
return nil
}
func checkActive(t *IntrospectionResponseBody) bool {
if t.Active != true {
return false
}
now := time.Now()
if t.Exp < now.Unix() {
return false
}
if t.Iat > now.Unix() {
return false
}
return true
}
func doIntrospectAccessToken(client *http.Client, endpoint string, clientId string, clientSecret string, token string) (*eosc.Base[IntrospectionResponseBody], error) {
body := url.Values{}
body.Set("token", token)
body.Set("client_id", clientId)
body.Set("client_secret", clientSecret)
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(body.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(data))
}
t := new(eosc.Base[IntrospectionResponseBody])
err = json.Unmarshal(data, t)
if err != nil {
return nil, err
}
return t, nil
}
func retrieveAccessToken(ctx http_service.IHttpContext, tokenPosition string, tokenName string) string {
token := ""
switch tokenPosition {
case positionHeader:
token = ctx.Request().Header().GetHeader(tokenName)
return strings.TrimPrefix(token, "Bearer ")
case positionQuery:
token = ctx.Request().URI().GetQuery(tokenName)
case positionBody:
if strings.Contains(ctx.Request().ContentType(), "application/x-www-form-urlencoded") || strings.Contains(ctx.Request().ContentType(), "multipart/form-data") {
token = ctx.Request().Body().GetForm(tokenName)
} else if strings.Contains(ctx.Request().ContentType(), "application/json") {
body, _ := ctx.Request().Body().RawBody()
if string(body) != "" {
m := make(map[string]interface{})
err := json.Unmarshal(body, &m)
if err == nil {
if v, ok := m[tokenName]; ok {
token = fmt.Sprintf("%v", v)
}
} else {
return ""
}
}
}
default:
return ""
}
return strings.TrimPrefix(token, "Bearer ")
}