Archived
实现首轮协议与 Transit 签名 PoC
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
|
||||
JWTTokenType = "urn:ietf:params:oauth:token-type:jwt"
|
||||
AccessTokenType = "urn:ietf:params:oauth:token-type:access_token"
|
||||
)
|
||||
|
||||
var clientIDPattern = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$`)
|
||||
|
||||
type ExchangeRequest struct {
|
||||
ClientID string
|
||||
SubjectToken string
|
||||
SubjectTokenType string
|
||||
RequestedTokenType string
|
||||
Audience string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
type RequestError struct {
|
||||
Code string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (e *RequestError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Description)
|
||||
}
|
||||
|
||||
func ParseExchangeRequest(form url.Values) (ExchangeRequest, error) {
|
||||
request := ExchangeRequest{
|
||||
ClientID: form.Get("client_id"),
|
||||
SubjectToken: form.Get("subject_token"),
|
||||
SubjectTokenType: form.Get("subject_token_type"),
|
||||
RequestedTokenType: form.Get("requested_token_type"),
|
||||
Audience: form.Get("audience"),
|
||||
Scopes: strings.Fields(form.Get("scope")),
|
||||
}
|
||||
|
||||
if form.Get("grant_type") != TokenExchangeGrantType {
|
||||
return ExchangeRequest{}, requestError("unsupported_grant_type", "unsupported grant_type")
|
||||
}
|
||||
if !clientIDPattern.MatchString(request.ClientID) {
|
||||
return ExchangeRequest{}, requestError("invalid_request", "client_id must use 3-63 lowercase letters, digits, or interior hyphens")
|
||||
}
|
||||
if request.SubjectToken == "" {
|
||||
return ExchangeRequest{}, requestError("invalid_request", "subject_token is required")
|
||||
}
|
||||
if request.SubjectTokenType != JWTTokenType {
|
||||
return ExchangeRequest{}, requestError("invalid_request", "unsupported subject_token_type")
|
||||
}
|
||||
if request.RequestedTokenType != AccessTokenType {
|
||||
return ExchangeRequest{}, requestError("invalid_request", "unsupported requested_token_type")
|
||||
}
|
||||
if request.Audience == "" || len(form["audience"]) != 1 {
|
||||
return ExchangeRequest{}, requestError("invalid_target", "exactly one audience is required")
|
||||
}
|
||||
if len(request.Scopes) == 0 {
|
||||
return ExchangeRequest{}, requestError("invalid_scope", "scope is required")
|
||||
}
|
||||
for _, unsupported := range []string{"resource", "actor_token", "actor_token_type"} {
|
||||
if form.Has(unsupported) {
|
||||
return ExchangeRequest{}, requestError("invalid_request", unsupported+" is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func requestError(code, description string) *RequestError {
|
||||
return &RequestError{Code: code, Description: description}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseExchangeRequest(t *testing.T) {
|
||||
request, err := ParseExchangeRequest(validForm())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseExchangeRequest() error = %v", err)
|
||||
}
|
||||
|
||||
if request.ClientID != "homelab-infra-ci" {
|
||||
t.Fatalf("ClientID = %q", request.ClientID)
|
||||
}
|
||||
if !reflect.DeepEqual(request.Scopes, []string{"bao.login", "ssh.certificate"}) {
|
||||
t.Fatalf("Scopes = %#v", request.Scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExchangeRequestRejectsInvalidInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(url.Values)
|
||||
code string
|
||||
}{
|
||||
{name: "missing client id", mutate: func(v url.Values) { v.Del("client_id") }, code: "invalid_request"},
|
||||
{name: "short client id", mutate: func(v url.Values) { v.Set("client_id", "ci") }, code: "invalid_request"},
|
||||
{name: "uppercase client id", mutate: func(v url.Values) { v.Set("client_id", "CI-worker") }, code: "invalid_request"},
|
||||
{name: "leading hyphen", mutate: func(v url.Values) { v.Set("client_id", "-ci-worker") }, code: "invalid_request"},
|
||||
{name: "missing token", mutate: func(v url.Values) { v.Del("subject_token") }, code: "invalid_request"},
|
||||
{name: "wrong grant", mutate: func(v url.Values) { v.Set("grant_type", "client_credentials") }, code: "unsupported_grant_type"},
|
||||
{name: "missing audience", mutate: func(v url.Values) { v.Del("audience") }, code: "invalid_target"},
|
||||
{name: "multiple audiences", mutate: func(v url.Values) { v.Add("audience", "second") }, code: "invalid_target"},
|
||||
{name: "missing scope", mutate: func(v url.Values) { v.Del("scope") }, code: "invalid_scope"},
|
||||
{name: "actor token", mutate: func(v url.Values) { v.Set("actor_token", "not-supported") }, code: "invalid_request"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
form := validForm()
|
||||
test.mutate(form)
|
||||
|
||||
_, err := ParseExchangeRequest(form)
|
||||
var requestErr *RequestError
|
||||
if !errors.As(err, &requestErr) {
|
||||
t.Fatalf("error = %v, want RequestError", err)
|
||||
}
|
||||
if requestErr.Code != test.code {
|
||||
t.Fatalf("error code = %q, want %q", requestErr.Code, test.code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validForm() url.Values {
|
||||
return url.Values{
|
||||
"grant_type": {TokenExchangeGrantType},
|
||||
"client_id": {"homelab-infra-ci"},
|
||||
"subject_token": {"test-subject-token"},
|
||||
"subject_token_type": {JWTTokenType},
|
||||
"requested_token_type": {AccessTokenType},
|
||||
"audience": {"https://bao.ad.ddupan.top:8200"},
|
||||
"scope": {"bao.login ssh.certificate"},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user