this repo has no description

more stuff

+1 -1
Makefile
···
.PHONY: test
test: ## Run tests
-
go test ./...
+
go test -v ./...
.PHONY: coverage-html
coverage-html: ## Generate test coverage report and open in browser
+77
generic.go
···
+
package oauth
+
+
import (
+
"crypto/ecdsa"
+
"crypto/elliptic"
+
"crypto/rand"
+
"fmt"
+
"net/url"
+
"time"
+
+
"github.com/lestrrat-go/jwx/v2/jwk"
+
)
+
+
func GenerateKey(kidPrefix *string) (jwk.Key, error) {
+
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+
if err != nil {
+
return nil, err
+
}
+
+
key, err := jwk.FromRaw(privKey)
+
if err != nil {
+
return nil, err
+
}
+
+
if kidPrefix != nil {
+
kid := fmt.Sprintf("%s-%d", *kidPrefix, time.Now().Unix())
+
+
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
+
return nil, err
+
}
+
}
+
+
return key, nil
+
}
+
+
func isSafeAndParsed(ustr string) (*url.URL, error) {
+
u, err := url.Parse(ustr)
+
if err != nil {
+
return nil, err
+
}
+
+
if u.Scheme != "https" {
+
return nil, fmt.Errorf("input url is not https")
+
}
+
+
if u.Hostname() == "" {
+
return nil, fmt.Errorf("url hostname was empty")
+
}
+
+
if u.User != nil {
+
return nil, fmt.Errorf("url user was not empty")
+
}
+
+
if u.Port() != "" {
+
return nil, fmt.Errorf("url port was not empty")
+
}
+
+
return u, nil
+
}
+
+
func getPrivateKey(key jwk.Key) (*ecdsa.PrivateKey, error) {
+
var pkey ecdsa.PrivateKey
+
if err := key.Raw(&pkey); err != nil {
+
return nil, err
+
}
+
+
return &pkey, nil
+
}
+
+
func getPublicKey(key jwk.Key) (*ecdsa.PublicKey, error) {
+
var pkey ecdsa.PublicKey
+
if err := key.Raw(&pkey); err != nil {
+
return nil, err
+
}
+
+
return &pkey, nil
+
}
+2 -1
go.mod
···
go 1.24.0
require (
+
github.com/golang-jwt/jwt/v5 v5.2.1
+
github.com/google/uuid v1.4.0
github.com/lestrrat-go/jwx/v2 v2.0.12
github.com/stretchr/testify v1.10.0
)
···
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
-
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
+2
go.sum
···
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+199 -16
oauth.go
···
package oauth
import (
+
"bytes"
"context"
+
"crypto/ecdsa"
+
"crypto/rand"
+
"crypto/sha256"
+
"encoding/base64"
+
"encoding/hex"
+
"encoding/json"
"fmt"
"io"
"net/http"
-
"net/url"
"time"
+
+
"github.com/golang-jwt/jwt/v5"
+
"github.com/google/uuid"
+
"github.com/lestrrat-go/jwx/v2/jwk"
)
type OauthClient struct {
-
h *http.Client
+
h *http.Client
+
clientPrivateKey *ecdsa.PrivateKey
+
clientKid string
+
clientId string
+
redirectUri string
}
type OauthClientArgs struct {
-
h *http.Client
+
H *http.Client
+
ClientJwk []byte
+
ClientId string
+
RedirectUri string
}
-
func NewOauthClient(args OauthClientArgs) *OauthClient {
-
if args.h == nil {
-
args.h = &http.Client{
+
func NewOauthClient(args OauthClientArgs) (*OauthClient, error) {
+
if args.ClientId == "" {
+
return nil, fmt.Errorf("no client id provided")
+
}
+
+
if args.RedirectUri == "" {
+
return nil, fmt.Errorf("no redirect uri provided")
+
}
+
+
if args.H == nil {
+
args.H = &http.Client{
Timeout: 5 * time.Second,
}
}
+
+
clientJwk, err := jwk.ParseKey(args.ClientJwk)
+
if err != nil {
+
return nil, err
+
}
+
+
clientPkey, err := getPrivateKey(clientJwk)
+
if err != nil {
+
return nil, fmt.Errorf("could not load private key from provided client jwk: %w", err)
+
}
+
+
kid := clientJwk.KeyID()
+
return &OauthClient{
-
h: args.h,
-
}
+
h: args.H,
+
clientKid: kid,
+
clientPrivateKey: clientPkey,
+
clientId: args.ClientId,
+
redirectUri: args.RedirectUri,
+
}, nil
}
func (o *OauthClient) ResolvePDSAuthServer(ctx context.Context, ustr string) (string, error) {
···
return metadata, nil
}
-
// func ClientAssertionJwt(clientId, authServerUrl string, clientSecretJwk jwk.Key) {
-
// clientAssertion := jwt.NewBuilder().Issuer(clientId).Subject(clientId).Audience(authServerUrl).IssuedAt(time.Now().Add()
-
// }
+
func (o *OauthClient) ClientAssertionJwt(authServerUrl string) (string, error) {
+
claims := jwt.MapClaims{
+
"iss": o.clientId,
+
"sub": o.clientId,
+
"aud": authServerUrl,
+
"jti": uuid.NewString(),
+
"iat": time.Now().Unix(),
+
}
+
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
+
token.Header["kid"] = o.clientKid
+
+
tokenString, err := token.SignedString(o.clientPrivateKey)
+
if err != nil {
+
return "", err
+
}
+
+
return tokenString, nil
+
}
+
+
func (o *OauthClient) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) {
+
raw, err := jwk.PublicKeyOf(privateJwk)
+
if err != nil {
+
return "", err
+
}
+
+
pubJwk, err := jwk.FromRaw(raw)
+
if err != nil {
+
return "", err
+
}
+
+
b, err := json.Marshal(pubJwk)
+
if err != nil {
+
return "", err
+
}
+
+
var pubMap map[string]interface{}
+
if err := json.Unmarshal(b, &pubMap); err != nil {
+
return "", err
+
}
+
+
now := time.Now().Unix()
+
+
claims := jwt.MapClaims{
+
"jti": uuid.NewString(),
+
"htm": method,
+
"htu": url,
+
"iat": now,
+
"exp": now + 30,
+
}
+
+
if nonce != "" {
+
claims["nonce"] = nonce
+
}
+
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
+
token.Header["typ"] = "dpop+jwt"
+
token.Header["alg"] = "ES256"
+
token.Header["jwk"] = pubMap
+
+
var rawKey interface{}
+
if err := privateJwk.Raw(&rawKey); err != nil {
+
return "", err
+
}
+
+
tokenString, err := token.SignedString(rawKey)
+
if err != nil {
+
return "", fmt.Errorf("failed to sign token: %w", err)
+
}
+
+
return tokenString, nil
+
}
+
+
func (o *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (any, error) {
+
if authServerMeta == nil {
+
return nil, fmt.Errorf("nil metadata provided")
+
}
+
+
parUrl := authServerMeta.PushedAuthorizationRequestEndpoint
+
+
state, err := generateToken(10)
+
if err != nil {
+
return nil, fmt.Errorf("could not generate state token: %w", err)
+
}
+
+
pkceVerifier, err := generateToken(48)
+
if err != nil {
+
return nil, fmt.Errorf("could not generate pkce verifier: %w", err)
+
}
+
+
codeChallenge := generateCodeChallenge(pkceVerifier)
+
codeChallengeMethod := "S256"
+
+
clientAssertion, err := o.ClientAssertionJwt(authServerUrl)
+
if err != nil {
+
return nil, err
+
}
+
+
// TODO: ??
+
nonce := ""
+
dpopProof, err := o.AuthServerDpopJwt("POST", parUrl, nonce, dpopPrivateKey)
+
if err != nil {
+
return nil, err
+
}
+
+
parBody := map[string]string{
+
"response_type": "code",
+
"code_challenge": codeChallenge,
+
"code_challenge_method": codeChallengeMethod,
+
"client_id": o.clientId,
+
"state": state,
+
"redirect_uri": o.redirectUri,
+
"scope": scope,
+
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+
"client_assertion": clientAssertion,
+
}
+
+
if loginHint != "" {
+
parBody["login_hint"] = loginHint
+
}
+
+
_, err = isSafeAndParsed(parUrl)
+
if err != nil {
+
return nil, err
+
}
+
+
b, err := json.Marshal(parBody)
+
if err != nil {
+
return nil, err
+
}
-
func isSafeAndParsed(ustr string) (*url.URL, error) {
-
u, err := url.Parse(ustr)
+
req, err := http.NewRequestWithContext(ctx, "POST", parUrl, bytes.NewReader(b))
if err != nil {
return nil, err
}
-
if u.Scheme != "https" {
-
return nil, fmt.Errorf("input url is not https")
+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
req.Header.Add("DPoP", dpopProof)
+
+
return nil, nil
+
}
+
+
func generateToken(len int) (string, error) {
+
b := make([]byte, len)
+
if _, err := rand.Read(b); err != nil {
+
return "", err
}
-
return u, nil
+
return hex.EncodeToString(b), nil
+
}
+
+
func generateCodeChallenge(pkceVerifier string) string {
+
h := sha256.New()
+
h.Write([]byte(pkceVerifier))
+
hash := h.Sum(nil)
+
return base64.RawURLEncoding.EncodeToString(hash)
}
+32 -1
oauth_test.go
···
import (
"context"
+
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
···
var (
ctx = context.Background()
-
oauthClient = NewOauthClient(OauthClientArgs{})
+
oauthClient = newTestOauthClient()
)
+
func newTestOauthClient() *OauthClient {
+
prefix := "testing"
+
testKey, err := GenerateKey(&prefix)
+
if err != nil {
+
panic(err)
+
}
+
+
b, err := json.Marshal(testKey)
+
if err != nil {
+
panic(err)
+
}
+
+
c, err := NewOauthClient(OauthClientArgs{
+
ClientJwk: b,
+
})
+
if err != nil {
+
panic(err)
+
}
+
+
return c
+
}
+
func TestResolvePDSAuthServer(t *testing.T) {
assert := assert.New(t)
···
assert.NoError(err)
assert.IsType(OauthAuthorizationMetadata{}, meta)
}
+
+
func TestGenerateKey(t *testing.T) {
+
assert := assert.New(t)
+
+
prefix := "testing"
+
_, err := GenerateKey(&prefix)
+
assert.NoError(err)
+
}
-20
types.go
···
iu, err := url.Parse(oam.Issuer)
if err != nil {
-
oam = nil
return err
}
if iu.Hostname() != fetch_url.Hostname() {
-
oam = nil
return fmt.Errorf("issuer hostname does not match fetch url hostname")
}
if iu.Scheme != "https" {
-
oam = nil
return fmt.Errorf("issuer url is not https")
}
if iu.Port() != "" {
-
oam = nil
return fmt.Errorf("issuer port is not empty")
}
if iu.Path != "" && iu.Path != "/" {
-
oam = nil
return fmt.Errorf("issuer path is not /")
}
if iu.RawQuery != "" {
-
oam = nil
return fmt.Errorf("issuer url params are not empty")
}
if !tokenInSet("code", oam.ResponseTypesSupported) {
-
oam = nil
return fmt.Errorf("`code` is not in response_types_supported")
}
if !tokenInSet("authorization_code", oam.GrantTypesSupported) {
-
oam = nil
return fmt.Errorf("`authorization_code` is not in grant_types_supported")
}
if !tokenInSet("refresh_token", oam.GrantTypesSupported) {
-
oam = nil
return fmt.Errorf("`refresh_token` is not in grant_types_supported")
}
if !tokenInSet("S256", oam.CodeChallengeMethodsSupported) {
-
oam = nil
return fmt.Errorf("`S256` is not in code_challenge_methods_supported")
}
if !tokenInSet("none", oam.TokenEndpointAuthMethodsSupported) {
-
oam = nil
return fmt.Errorf("`none` is not in token_endpoint_auth_methods_supported")
}
if !tokenInSet("private_key_jwt", oam.TokenEndpointAuthMethodsSupported) {
-
oam = nil
return fmt.Errorf("`private_key_jwt` is not in token_endpoint_auth_methods_supported")
}
if !tokenInSet("ES256", oam.TokenEndpointAuthSigningAlgValuesSupported) {
-
oam = nil
return fmt.Errorf("`ES256` is not in token_endpoint_auth_signing_alg_values_supported")
}
if !tokenInSet("atproto", oam.ScopesSupported) {
-
oam = nil
return fmt.Errorf("`atproto` is not in scopes_supported")
}
if oam.AuthorizationResponseISSParameterSupported != true {
-
oam = nil
return fmt.Errorf("authorization_response_iss_parameter_supported is not true")
}
if oam.PushedAuthorizationRequestEndpoint == "" {
-
oam = nil
return fmt.Errorf("pushed_authorization_request_endpoint is empty")
}
if oam.RequirePushedAuthorizationRequests == false {
-
oam = nil
return fmt.Errorf("require_pushed_authorization_requests is false")
}
if !tokenInSet("ES256", oam.DpopSigningAlgValuesSupported) {
-
oam = nil
return fmt.Errorf("`ES256` is not in dpop_signing_alg_values_supported")
}
if oam.RequireRequestUriRegistration != nil && *oam.RequireRequestUriRegistration == false {
-
oam = nil
return fmt.Errorf("require_request_uri_registration present in metadata and was false")
}
if oam.ClientIDMetadataDocumentSupported == false {
-
oam = nil
return fmt.Errorf("client_id_metadata_document_supported was false")
}