this repo has no description

support signin with wpds url

Changed files
+135 -111
cmd
client_test
+42 -33
cmd/client_test/handle_auth.go
···
"encoding/json"
"fmt"
"net/url"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
···
)
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
-
handle := e.FormValue("handle")
-
if handle == "" {
-
return e.Redirect(302, "/login?e=handle-empty")
}
-
_, herr := syntax.ParseHandle(handle)
-
_, derr := syntax.ParseDID(handle)
-
if herr != nil && derr != nil {
-
return e.Redirect(302, "/login?e=handle-invalid")
-
}
-
var did string
-
if derr == nil {
-
did = handle
-
} else {
-
maybeDid, err := resolveHandle(e.Request().Context(), handle)
if err != nil {
return err
}
-
did = maybeDid
-
}
-
-
service, err := resolveService(ctx, did)
-
if err != nil {
-
return err
}
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
···
}
sessState := sess.Values["oauth_state"]
-
sessDid := sess.Values["oauth_did"]
-
if resState == "" || resIss == "" || resCode == "" || sessState == "" || sessDid == "" {
return fmt.Errorf("request missing needed parameters")
}
···
}
var oauthRequest OauthRequest
-
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Scan(&oauthRequest).Error; err != nil {
return err
}
-
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Error; err != nil {
return err
}
···
return err
}
-
initialTokenResp, err := s.oauthClient.InitialTokenRequest(
-
e.Request().Context(),
-
resCode,
-
resIss,
-
oauthRequest.PkceVerifier,
-
oauthRequest.DpopAuthserverNonce,
-
jwk,
-
)
if err != nil {
return err
}
-
-
// TODO: resolve if needed
if initialTokenResp.Scope != scope {
return fmt.Errorf("did not receive correct scopes from token request")
}
oauthSession := &OauthSession{
···
"encoding/json"
"fmt"
"net/url"
+
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
···
)
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
+
authInput := e.FormValue("auth-input")
+
if authInput == "" {
+
return e.Redirect(302, "/login?e=auth-input-empty")
}
+
var service string
+
var did string
+
if strings.HasPrefix("https://", authInput) {
+
u, err := url.Parse(authInput)
+
if err == nil {
+
u.Path = ""
+
u.RawQuery = ""
+
u.User = nil
+
service = u.String()
+
}
+
} else {
+
_, herr := syntax.ParseHandle(authInput)
+
_, derr := syntax.ParseDID(authInput)
+
if herr != nil && derr != nil {
+
return e.Redirect(302, "/login?e=handle-invalid")
+
}
+
+
if derr == nil {
+
did = authInput
+
} else {
+
maybeDid, err := resolveHandle(e.Request().Context(), authInput)
+
if err != nil {
+
return err
+
}
+
+
did = maybeDid
+
}
+
maybeService, err := resolveService(ctx, did)
if err != nil {
return err
}
+
service = maybeService
}
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
···
}
sessState := sess.Values["oauth_state"]
+
if resState == "" || resIss == "" || resCode == "" {
return fmt.Errorf("request missing needed parameters")
}
···
}
var oauthRequest OauthRequest
+
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ?", sessState).Scan(&oauthRequest).Error; err != nil {
return err
}
+
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ?", sessState).Error; err != nil {
return err
}
···
return err
}
+
initialTokenResp, err := s.oauthClient.InitialTokenRequest(e.Request().Context(), resCode, resIss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
if err != nil {
return err
}
if initialTokenResp.Scope != scope {
return fmt.Errorf("did not receive correct scopes from token request")
+
}
+
+
// if we didn't start with a did, we can get it from the response
+
if oauthRequest.Did == "" {
+
oauthRequest.Did = initialTokenResp.Sub
}
oauthSession := &OauthSession{
+5 -1
cmd/client_test/html/login.html
···
<!doctype html>
<html>
<form action="/login" method="post">
-
<input name="handle" id="handle" placeholder="Enter your handle" />
<button type="submit" value="Submit">Submit</button>
</form>
</html>
···
<!doctype html>
<html>
<form action="/login" method="post">
+
<input
+
name="auth-input"
+
id="auth-input"
+
placeholder="Enter your handle, did, or pds url"
+
/>
<button type="submit" value="Submit">Submit</button>
</form>
</html>
+2 -2
cmd/client_test/user.go
···
return nil, false, err
}
-
did, ok := sess.Values["did"]
if !ok {
return nil, false, nil
}
-
oauthSession, err := s.getOauthSession(e.Request().Context(), did.(string))
privateJwk, err := oauth.ParseKeyFromBytes([]byte(oauthSession.DpopPrivateJwk))
if err != nil {
···
return nil, false, err
}
+
did, ok := sess.Values["did"].(string)
if !ok {
return nil, false, nil
}
+
oauthSession, err := s.getOauthSession(e.Request().Context(), did)
privateJwk, err := oauth.ParseKeyFromBytes([]byte(oauthSession.DpopPrivateJwk))
if err != nil {
+19
generic.go
···
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net/url"
"time"
···
func ParseKeyFromBytes(b []byte) (jwk.Key, error) {
return jwk.ParseKey(b)
}
···
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
+
"crypto/sha256"
+
"encoding/base64"
+
"encoding/hex"
"fmt"
"net/url"
"time"
···
func ParseKeyFromBytes(b []byte) (jwk.Key, error) {
return jwk.ParseKey(b)
}
+
+
func generateToken(len int) (string, error) {
+
b := make([]byte, len)
+
if _, err := rand.Read(b); err != nil {
+
return "", err
+
}
+
+
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)
+
}
+67 -74
oauth.go
···
import (
"context"
"crypto/ecdsa"
-
"crypto/rand"
-
"crypto/sha256"
-
"encoding/base64"
-
"encoding/hex"
"encoding/json"
"fmt"
"io"
···
}
type ClientArgs struct {
-
H *http.Client
ClientJwk jwk.Key
ClientId string
RedirectUri string
···
return nil, fmt.Errorf("no redirect uri provided")
}
-
if args.H == nil {
-
args.H = &http.Client{
Timeout: 5 * time.Second,
}
}
···
kid := args.ClientJwk.KeyID()
return &Client{
-
h: args.H,
clientKid: kid,
clientPrivateKey: clientPkey,
clientId: args.ClientId,
···
resp, err := c.h.Do(req)
if err != nil {
-
return nil, fmt.Errorf("error getting response for auth metadata: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
-
return nil, fmt.Errorf(
-
"received non-200 response from pds. status code was %d",
-
resp.StatusCode,
-
)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
-
return nil, fmt.Errorf("could not read body for metadata response: %w", err)
}
var metadata OauthAuthorizationMetadata
if err := metadata.UnmarshalJSON(b); err != nil {
-
return nil, fmt.Errorf("could not unmarshal metadata: %w", err)
}
if err := metadata.Validate(u); err != nil {
-
return nil, fmt.Errorf("could not validate metadata: %w", err)
}
return &metadata, nil
···
dpopAuthserverNonce string,
dpopPrivateJwk jwk.Key,
) (*TokenResponse, error) {
-
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
-
if err != nil {
-
return nil, err
-
}
-
clientAssertion, err := c.ClientAssertionJwt(authserverIss)
-
if err != nil {
-
return nil, err
-
}
-
params := url.Values{
-
"client_id": {c.clientId},
-
"redirect_uri": {c.redirectUri},
-
"grant_type": {"authorization_code"},
-
"code": {code},
-
"code_verifier": {pkceVerifier},
-
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
-
"client_assertion": {clientAssertion},
-
}
-
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
-
if err != nil {
-
return nil, err
-
}
-
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
-
if err != nil {
-
return nil, err
-
}
-
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
req.Header.Set("DPoP", dpopProof)
-
resp, err := c.h.Do(req)
-
if err != nil {
-
return nil, err
-
}
-
defer resp.Body.Close()
-
var tokenResponse TokenResponse
-
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
-
return nil, err
}
-
tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
-
-
return &tokenResponse, nil
}
func (c *Client) RefreshTokenRequest(
···
return nil, nil
}
-
-
func generateToken(len int) (string, error) {
-
b := make([]byte, len)
-
if _, err := rand.Read(b); err != nil {
-
return "", err
-
}
-
-
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)
-
}
-
-
func parsePrivateJwkFromString(str string) (jwk.Key, error) {
-
return jwk.ParseKey([]byte(str))
-
}
···
import (
"context"
"crypto/ecdsa"
"encoding/json"
"fmt"
"io"
···
}
type ClientArgs struct {
+
Http *http.Client
ClientJwk jwk.Key
ClientId string
RedirectUri string
···
return nil, fmt.Errorf("no redirect uri provided")
}
+
if args.Http == nil {
+
args.Http = &http.Client{
Timeout: 5 * time.Second,
}
}
···
kid := args.ClientJwk.KeyID()
return &Client{
+
h: args.Http,
clientKid: kid,
clientPrivateKey: clientPkey,
clientId: args.ClientId,
···
resp, err := c.h.Do(req)
if err != nil {
+
return nil, fmt.Errorf("error getting response for authserver metadata: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
+
return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
+
return nil, fmt.Errorf("could not read body for authserver metadata response: %w", err)
}
var metadata OauthAuthorizationMetadata
if err := metadata.UnmarshalJSON(b); err != nil {
+
return nil, fmt.Errorf("could not unmarshal authserver metadata: %w", err)
}
if err := metadata.Validate(u); err != nil {
+
return nil, fmt.Errorf("could not validate authserver metadata: %w", err)
}
return &metadata, nil
···
dpopAuthserverNonce string,
dpopPrivateJwk jwk.Key,
) (*TokenResponse, error) {
+
// we might need to re-run to update dpop nonce
+
for range 2 {
+
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
+
if err != nil {
+
return nil, err
+
}
+
+
clientAssertion, err := c.ClientAssertionJwt(authserverIss)
+
if err != nil {
+
return nil, err
+
}
+
params := url.Values{
+
"client_id": {c.clientId},
+
"redirect_uri": {c.redirectUri},
+
"grant_type": {"authorization_code"},
+
"code": {code},
+
"code_verifier": {pkceVerifier},
+
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
+
"client_assertion": {clientAssertion},
+
}
+
+
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
+
if err != nil {
+
return nil, err
+
}
+
+
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
+
if err != nil {
+
return nil, err
+
}
+
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
req.Header.Set("DPoP", dpopProof)
+
+
resp, err := c.h.Do(req)
+
if err != nil {
+
return nil, err
+
}
+
defer resp.Body.Close()
+
if resp.StatusCode != 200 && resp.StatusCode != 201 {
+
var respMap map[string]string
+
if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil {
+
return nil, err
+
}
+
if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" {
+
dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
+
continue
+
}
+
return nil, fmt.Errorf("token refresh error: %s", respMap["error"])
+
}
+
var tokenResponse TokenResponse
+
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
+
return nil, err
+
}
+
// set nonce so the updates are reflected in the response
+
tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
+
return &tokenResponse, nil
}
+
return nil, nil
}
func (c *Client) RefreshTokenRequest(
···
return nil, nil
}
-1
oauth_test.go
···
}
// make sure the server is running
-
req, err := http.NewRequest("GET", serverMetadataUrl, nil)
if err != nil {
panic(err)
···
}
// make sure the server is running
req, err := http.NewRequest("GET", serverMetadataUrl, nil)
if err != nil {
panic(err)