this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

more stuff

Hailey 4982663b 3f5828ba

+313 -39
+1 -1
Makefile
··· 16 16 17 17 .PHONY: test 18 18 test: ## Run tests 19 - go test ./... 19 + go test -v ./... 20 20 21 21 .PHONY: coverage-html 22 22 coverage-html: ## Generate test coverage report and open in browser
+77
generic.go
··· 1 + package oauth 2 + 3 + import ( 4 + "crypto/ecdsa" 5 + "crypto/elliptic" 6 + "crypto/rand" 7 + "fmt" 8 + "net/url" 9 + "time" 10 + 11 + "github.com/lestrrat-go/jwx/v2/jwk" 12 + ) 13 + 14 + func GenerateKey(kidPrefix *string) (jwk.Key, error) { 15 + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 16 + if err != nil { 17 + return nil, err 18 + } 19 + 20 + key, err := jwk.FromRaw(privKey) 21 + if err != nil { 22 + return nil, err 23 + } 24 + 25 + if kidPrefix != nil { 26 + kid := fmt.Sprintf("%s-%d", *kidPrefix, time.Now().Unix()) 27 + 28 + if err := key.Set(jwk.KeyIDKey, kid); err != nil { 29 + return nil, err 30 + } 31 + } 32 + 33 + return key, nil 34 + } 35 + 36 + func isSafeAndParsed(ustr string) (*url.URL, error) { 37 + u, err := url.Parse(ustr) 38 + if err != nil { 39 + return nil, err 40 + } 41 + 42 + if u.Scheme != "https" { 43 + return nil, fmt.Errorf("input url is not https") 44 + } 45 + 46 + if u.Hostname() == "" { 47 + return nil, fmt.Errorf("url hostname was empty") 48 + } 49 + 50 + if u.User != nil { 51 + return nil, fmt.Errorf("url user was not empty") 52 + } 53 + 54 + if u.Port() != "" { 55 + return nil, fmt.Errorf("url port was not empty") 56 + } 57 + 58 + return u, nil 59 + } 60 + 61 + func getPrivateKey(key jwk.Key) (*ecdsa.PrivateKey, error) { 62 + var pkey ecdsa.PrivateKey 63 + if err := key.Raw(&pkey); err != nil { 64 + return nil, err 65 + } 66 + 67 + return &pkey, nil 68 + } 69 + 70 + func getPublicKey(key jwk.Key) (*ecdsa.PublicKey, error) { 71 + var pkey ecdsa.PublicKey 72 + if err := key.Raw(&pkey); err != nil { 73 + return nil, err 74 + } 75 + 76 + return &pkey, nil 77 + }
+2 -1
go.mod
··· 3 3 go 1.24.0 4 4 5 5 require ( 6 + github.com/golang-jwt/jwt/v5 v5.2.1 7 + github.com/google/uuid v1.4.0 6 8 github.com/lestrrat-go/jwx/v2 v2.0.12 7 9 github.com/stretchr/testify v1.10.0 8 10 ) ··· 12 14 github.com/davecgh/go-spew v1.1.1 // indirect 13 15 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 14 16 github.com/goccy/go-json v0.10.2 // indirect 15 - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 16 17 github.com/gorilla/context v1.1.2 // indirect 17 18 github.com/gorilla/securecookie v1.1.2 // indirect 18 19 github.com/gorilla/sessions v1.4.0 // indirect
+2
go.sum
··· 11 11 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 12 12 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 13 13 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 14 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 15 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 16 github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= 15 17 github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= 16 18 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+199 -16
oauth.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 6 + "crypto/ecdsa" 7 + "crypto/rand" 8 + "crypto/sha256" 9 + "encoding/base64" 10 + "encoding/hex" 11 + "encoding/json" 5 12 "fmt" 6 13 "io" 7 14 "net/http" 8 - "net/url" 9 15 "time" 16 + 17 + "github.com/golang-jwt/jwt/v5" 18 + "github.com/google/uuid" 19 + "github.com/lestrrat-go/jwx/v2/jwk" 10 20 ) 11 21 12 22 type OauthClient struct { 13 - h *http.Client 23 + h *http.Client 24 + clientPrivateKey *ecdsa.PrivateKey 25 + clientKid string 26 + clientId string 27 + redirectUri string 14 28 } 15 29 16 30 type OauthClientArgs struct { 17 - h *http.Client 31 + H *http.Client 32 + ClientJwk []byte 33 + ClientId string 34 + RedirectUri string 18 35 } 19 36 20 - func NewOauthClient(args OauthClientArgs) *OauthClient { 21 - if args.h == nil { 22 - args.h = &http.Client{ 37 + func NewOauthClient(args OauthClientArgs) (*OauthClient, error) { 38 + if args.ClientId == "" { 39 + return nil, fmt.Errorf("no client id provided") 40 + } 41 + 42 + if args.RedirectUri == "" { 43 + return nil, fmt.Errorf("no redirect uri provided") 44 + } 45 + 46 + if args.H == nil { 47 + args.H = &http.Client{ 23 48 Timeout: 5 * time.Second, 24 49 } 25 50 } 51 + 52 + clientJwk, err := jwk.ParseKey(args.ClientJwk) 53 + if err != nil { 54 + return nil, err 55 + } 56 + 57 + clientPkey, err := getPrivateKey(clientJwk) 58 + if err != nil { 59 + return nil, fmt.Errorf("could not load private key from provided client jwk: %w", err) 60 + } 61 + 62 + kid := clientJwk.KeyID() 63 + 26 64 return &OauthClient{ 27 - h: args.h, 28 - } 65 + h: args.H, 66 + clientKid: kid, 67 + clientPrivateKey: clientPkey, 68 + clientId: args.ClientId, 69 + redirectUri: args.RedirectUri, 70 + }, nil 29 71 } 30 72 31 73 func (o *OauthClient) ResolvePDSAuthServer(ctx context.Context, ustr string) (string, error) { ··· 110 152 return metadata, nil 111 153 } 112 154 113 - // func ClientAssertionJwt(clientId, authServerUrl string, clientSecretJwk jwk.Key) { 114 - // clientAssertion := jwt.NewBuilder().Issuer(clientId).Subject(clientId).Audience(authServerUrl).IssuedAt(time.Now().Add() 115 - // } 155 + func (o *OauthClient) ClientAssertionJwt(authServerUrl string) (string, error) { 156 + claims := jwt.MapClaims{ 157 + "iss": o.clientId, 158 + "sub": o.clientId, 159 + "aud": authServerUrl, 160 + "jti": uuid.NewString(), 161 + "iat": time.Now().Unix(), 162 + } 163 + 164 + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 165 + token.Header["kid"] = o.clientKid 166 + 167 + tokenString, err := token.SignedString(o.clientPrivateKey) 168 + if err != nil { 169 + return "", err 170 + } 171 + 172 + return tokenString, nil 173 + } 174 + 175 + func (o *OauthClient) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) { 176 + raw, err := jwk.PublicKeyOf(privateJwk) 177 + if err != nil { 178 + return "", err 179 + } 180 + 181 + pubJwk, err := jwk.FromRaw(raw) 182 + if err != nil { 183 + return "", err 184 + } 185 + 186 + b, err := json.Marshal(pubJwk) 187 + if err != nil { 188 + return "", err 189 + } 190 + 191 + var pubMap map[string]interface{} 192 + if err := json.Unmarshal(b, &pubMap); err != nil { 193 + return "", err 194 + } 195 + 196 + now := time.Now().Unix() 197 + 198 + claims := jwt.MapClaims{ 199 + "jti": uuid.NewString(), 200 + "htm": method, 201 + "htu": url, 202 + "iat": now, 203 + "exp": now + 30, 204 + } 205 + 206 + if nonce != "" { 207 + claims["nonce"] = nonce 208 + } 209 + 210 + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 211 + token.Header["typ"] = "dpop+jwt" 212 + token.Header["alg"] = "ES256" 213 + token.Header["jwk"] = pubMap 214 + 215 + var rawKey interface{} 216 + if err := privateJwk.Raw(&rawKey); err != nil { 217 + return "", err 218 + } 219 + 220 + tokenString, err := token.SignedString(rawKey) 221 + if err != nil { 222 + return "", fmt.Errorf("failed to sign token: %w", err) 223 + } 224 + 225 + return tokenString, nil 226 + } 227 + 228 + func (o *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (any, error) { 229 + if authServerMeta == nil { 230 + return nil, fmt.Errorf("nil metadata provided") 231 + } 232 + 233 + parUrl := authServerMeta.PushedAuthorizationRequestEndpoint 234 + 235 + state, err := generateToken(10) 236 + if err != nil { 237 + return nil, fmt.Errorf("could not generate state token: %w", err) 238 + } 239 + 240 + pkceVerifier, err := generateToken(48) 241 + if err != nil { 242 + return nil, fmt.Errorf("could not generate pkce verifier: %w", err) 243 + } 244 + 245 + codeChallenge := generateCodeChallenge(pkceVerifier) 246 + codeChallengeMethod := "S256" 247 + 248 + clientAssertion, err := o.ClientAssertionJwt(authServerUrl) 249 + if err != nil { 250 + return nil, err 251 + } 252 + 253 + // TODO: ?? 254 + nonce := "" 255 + dpopProof, err := o.AuthServerDpopJwt("POST", parUrl, nonce, dpopPrivateKey) 256 + if err != nil { 257 + return nil, err 258 + } 259 + 260 + parBody := map[string]string{ 261 + "response_type": "code", 262 + "code_challenge": codeChallenge, 263 + "code_challenge_method": codeChallengeMethod, 264 + "client_id": o.clientId, 265 + "state": state, 266 + "redirect_uri": o.redirectUri, 267 + "scope": scope, 268 + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 269 + "client_assertion": clientAssertion, 270 + } 271 + 272 + if loginHint != "" { 273 + parBody["login_hint"] = loginHint 274 + } 275 + 276 + _, err = isSafeAndParsed(parUrl) 277 + if err != nil { 278 + return nil, err 279 + } 280 + 281 + b, err := json.Marshal(parBody) 282 + if err != nil { 283 + return nil, err 284 + } 116 285 117 - func isSafeAndParsed(ustr string) (*url.URL, error) { 118 - u, err := url.Parse(ustr) 286 + req, err := http.NewRequestWithContext(ctx, "POST", parUrl, bytes.NewReader(b)) 119 287 if err != nil { 120 288 return nil, err 121 289 } 122 290 123 - if u.Scheme != "https" { 124 - return nil, fmt.Errorf("input url is not https") 291 + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 292 + req.Header.Add("DPoP", dpopProof) 293 + 294 + return nil, nil 295 + } 296 + 297 + func generateToken(len int) (string, error) { 298 + b := make([]byte, len) 299 + if _, err := rand.Read(b); err != nil { 300 + return "", err 125 301 } 126 302 127 - return u, nil 303 + return hex.EncodeToString(b), nil 304 + } 305 + 306 + func generateCodeChallenge(pkceVerifier string) string { 307 + h := sha256.New() 308 + h.Write([]byte(pkceVerifier)) 309 + hash := h.Sum(nil) 310 + return base64.RawURLEncoding.EncodeToString(hash) 128 311 }
+32 -1
oauth_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 5 6 "testing" 6 7 7 8 "github.com/stretchr/testify/assert" ··· 9 10 10 11 var ( 11 12 ctx = context.Background() 12 - oauthClient = NewOauthClient(OauthClientArgs{}) 13 + oauthClient = newTestOauthClient() 13 14 ) 14 15 16 + func newTestOauthClient() *OauthClient { 17 + prefix := "testing" 18 + testKey, err := GenerateKey(&prefix) 19 + if err != nil { 20 + panic(err) 21 + } 22 + 23 + b, err := json.Marshal(testKey) 24 + if err != nil { 25 + panic(err) 26 + } 27 + 28 + c, err := NewOauthClient(OauthClientArgs{ 29 + ClientJwk: b, 30 + }) 31 + if err != nil { 32 + panic(err) 33 + } 34 + 35 + return c 36 + } 37 + 15 38 func TestResolvePDSAuthServer(t *testing.T) { 16 39 assert := assert.New(t) 17 40 ··· 30 53 assert.NoError(err) 31 54 assert.IsType(OauthAuthorizationMetadata{}, meta) 32 55 } 56 + 57 + func TestGenerateKey(t *testing.T) { 58 + assert := assert.New(t) 59 + 60 + prefix := "testing" 61 + _, err := GenerateKey(&prefix) 62 + assert.NoError(err) 63 + }
-20
types.go
··· 65 65 66 66 iu, err := url.Parse(oam.Issuer) 67 67 if err != nil { 68 - oam = nil 69 68 return err 70 69 } 71 70 72 71 if iu.Hostname() != fetch_url.Hostname() { 73 - oam = nil 74 72 return fmt.Errorf("issuer hostname does not match fetch url hostname") 75 73 } 76 74 77 75 if iu.Scheme != "https" { 78 - oam = nil 79 76 return fmt.Errorf("issuer url is not https") 80 77 } 81 78 82 79 if iu.Port() != "" { 83 - oam = nil 84 80 return fmt.Errorf("issuer port is not empty") 85 81 } 86 82 87 83 if iu.Path != "" && iu.Path != "/" { 88 - oam = nil 89 84 return fmt.Errorf("issuer path is not /") 90 85 } 91 86 92 87 if iu.RawQuery != "" { 93 - oam = nil 94 88 return fmt.Errorf("issuer url params are not empty") 95 89 } 96 90 97 91 if !tokenInSet("code", oam.ResponseTypesSupported) { 98 - oam = nil 99 92 return fmt.Errorf("`code` is not in response_types_supported") 100 93 } 101 94 102 95 if !tokenInSet("authorization_code", oam.GrantTypesSupported) { 103 - oam = nil 104 96 return fmt.Errorf("`authorization_code` is not in grant_types_supported") 105 97 } 106 98 107 99 if !tokenInSet("refresh_token", oam.GrantTypesSupported) { 108 - oam = nil 109 100 return fmt.Errorf("`refresh_token` is not in grant_types_supported") 110 101 } 111 102 112 103 if !tokenInSet("S256", oam.CodeChallengeMethodsSupported) { 113 - oam = nil 114 104 return fmt.Errorf("`S256` is not in code_challenge_methods_supported") 115 105 } 116 106 117 107 if !tokenInSet("none", oam.TokenEndpointAuthMethodsSupported) { 118 - oam = nil 119 108 return fmt.Errorf("`none` is not in token_endpoint_auth_methods_supported") 120 109 } 121 110 122 111 if !tokenInSet("private_key_jwt", oam.TokenEndpointAuthMethodsSupported) { 123 - oam = nil 124 112 return fmt.Errorf("`private_key_jwt` is not in token_endpoint_auth_methods_supported") 125 113 } 126 114 127 115 if !tokenInSet("ES256", oam.TokenEndpointAuthSigningAlgValuesSupported) { 128 - oam = nil 129 116 return fmt.Errorf("`ES256` is not in token_endpoint_auth_signing_alg_values_supported") 130 117 } 131 118 132 119 if !tokenInSet("atproto", oam.ScopesSupported) { 133 - oam = nil 134 120 return fmt.Errorf("`atproto` is not in scopes_supported") 135 121 } 136 122 137 123 if oam.AuthorizationResponseISSParameterSupported != true { 138 - oam = nil 139 124 return fmt.Errorf("authorization_response_iss_parameter_supported is not true") 140 125 } 141 126 142 127 if oam.PushedAuthorizationRequestEndpoint == "" { 143 - oam = nil 144 128 return fmt.Errorf("pushed_authorization_request_endpoint is empty") 145 129 } 146 130 147 131 if oam.RequirePushedAuthorizationRequests == false { 148 - oam = nil 149 132 return fmt.Errorf("require_pushed_authorization_requests is false") 150 133 } 151 134 152 135 if !tokenInSet("ES256", oam.DpopSigningAlgValuesSupported) { 153 - oam = nil 154 136 return fmt.Errorf("`ES256` is not in dpop_signing_alg_values_supported") 155 137 } 156 138 157 139 if oam.RequireRequestUriRegistration != nil && *oam.RequireRequestUriRegistration == false { 158 - oam = nil 159 140 return fmt.Errorf("require_request_uri_registration present in metadata and was false") 160 141 } 161 142 162 143 if oam.ClientIDMetadataDocumentSupported == false { 163 - oam = nil 164 144 return fmt.Errorf("client_id_metadata_document_supported was false") 165 145 } 166 146