Something went wrong. Try again.
An atproto PDS written in Go
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245package server
import ( "errors" "fmt" "net/url" "slices" "strings" "time"
"github.com/Azure/go-autorest/autorest/to" "github.com/haileyok/cocoon/internal/helpers" "github.com/haileyok/cocoon/oauth" "github.com/haileyok/cocoon/oauth/constants" "github.com/haileyok/cocoon/oauth/provider" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4")
type HandleOauthAuthorizeGetInput struct { RequestUri string `query:"request_uri"`}
func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { ctx := e.Request().Context()
logger := s.logger.With("name", "handleOauthAuthorizeGet")
var input HandleOauthAuthorizeGetInput if err := e.Bind(&input); err != nil { logger.Error("error binding request", "err", err) return fmt.Errorf("error binding request") }
var reqId string if input.RequestUri != "" { id, err := oauth.DecodeRequestUri(input.RequestUri) if err != nil { logger.Error("no request uri found in input", "url", e.Request().URL.String()) return helpers.InputError(e, to.StringPtr("no request uri")) } reqId = id } else { var parRequest provider.ParRequest if err := e.Bind(&parRequest); err != nil { s.logger.Error("error binding for standard auth request", "error", err) return helpers.InputError(e, to.StringPtr("InvalidRequest")) }
if err := e.Validate(parRequest); err != nil { // render page for logged out dev if s.config.Version == "dev" && parRequest.ClientID == "" { return e.Render(200, "authorize.html", map[string]any{ "Scopes": []string{"atproto", "transition:generic"}, "AppName": "DEV MODE AUTHORIZATION PAGE", "Handle": "paula.cocoon.social", "RequestUri": "", "Accounts": []string{}, "ActiveDid": "", }) } return helpers.InputError(e, to.StringPtr("no request uri and invalid parameters")) }
client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, nil, &provider.AuthenticateClientOptions{ AllowMissingDpopProof: true, }) if err != nil { s.logger.Error("error authenticating client in standard request", "client_id", parRequest.ClientID, "error", err) return helpers.ServerError(e, to.StringPtr(err.Error())) }
if parRequest.DpopJkt == nil { if client.Metadata.DpopBoundAccessTokens { } } else { if !client.Metadata.DpopBoundAccessTokens { msg := "dpop bound access tokens are not enabled for this client" return helpers.InputError(e, &msg) } }
eat := time.Now().Add(constants.ParExpiresIn) id := oauth.GenerateRequestId()
authRequest := &provider.OauthAuthorizationRequest{ RequestId: id, ClientId: client.Metadata.ClientID, ClientAuth: *clientAuth, Parameters: parRequest, ExpiresAt: eat, }
if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { s.logger.Error("error creating auth request in db", "error", err) return helpers.ServerError(e, nil) }
input.RequestUri = oauth.EncodeRequestUri(id) reqId = id
}
var req provider.OauthAuthorizationRequest if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) }
clientId := e.QueryParam("client_id") if clientId != req.ClientId { return helpers.InputError(e, to.StringPtr("client id does not match the client id for the supplied request")) }
client, err := s.oauthProvider.ClientManager.GetClient(e.Request().Context(), req.ClientId) if err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) }
sess, err := session.Get(s.config.SessionCookieKey, e) if err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) }
if req.Parameters.LoginHint != nil && *req.Parameters.LoginHint != "" { did, err := s.resolveLoginHintToDid(ctx, *req.Parameters.LoginHint) if err != nil || !slices.Contains(getSessionDids(sess), did) { return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) }
setActiveSessionDid(sess, did) applyAccountSessionOptions(sess, int(AccountSessionMaxAge.Seconds())) if err := sess.Save(e.Request(), e.Response()); err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) } }
repo, _, accounts, err := s.getSessionRepoAndAccountsFromSessionOrErr(e, ctx, sess) if err != nil { if !errors.Is(err, ErrSessionUnauthenticated) { return helpers.ServerError(e, to.StringPtr(err.Error())) } return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) }
scopes := strings.Split(req.Parameters.Scope, " ") appName := client.Metadata.ClientName
data := map[string]any{ "Scopes": scopes, "AppName": appName, "RequestUri": input.RequestUri, "QueryParams": e.QueryParams().Encode(), "Handle": repo.Actor.Handle, "Accounts": accounts, "ActiveDid": repo.Repo.Did, }
return e.Render(200, "authorize.html", data)}
type OauthAuthorizePostRequest struct { RequestUri string `form:"request_uri"` AcceptOrRejct string `form:"accept_or_reject"`}
func (s *Server) handleOauthAuthorizePost(e echo.Context) error { ctx := e.Request().Context() logger := s.logger.With("name", "handleOauthAuthorizePost")
repo, _, err := s.getSessionRepoOrErr(e) if err != nil { if !errors.Is(err, ErrSessionUnauthenticated) { return helpers.ServerError(e, to.StringPtr(err.Error())) } return e.Redirect(303, "/account/signin") }
var req OauthAuthorizePostRequest if err := e.Bind(&req); err != nil { logger.Error("error binding authorize post request", "error", err) return helpers.InputError(e, nil) }
reqId, err := oauth.DecodeRequestUri(req.RequestUri) if err != nil { return helpers.InputError(e, to.StringPtr(err.Error())) }
var authReq provider.OauthAuthorizationRequest if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) }
client, err := s.oauthProvider.ClientManager.GetClient(e.Request().Context(), authReq.ClientId) if err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) }
// TODO: figure out how im supposed to actually redirect if req.AcceptOrRejct == "reject" { return e.Redirect(303, client.Metadata.ClientURI) }
if time.Now().After(authReq.ExpiresAt) { return helpers.InputError(e, to.StringPtr("the request has expired")) }
if authReq.Sub != nil || authReq.Code != nil { return helpers.InputError(e, to.StringPtr("this request was already authorized")) }
code := oauth.GenerateCode()
if err := s.db.Exec(ctx, "UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil { logger.Error("error updating authorization request", "error", err) return helpers.ServerError(e, nil) }
q := url.Values{} q.Set("state", authReq.Parameters.State) q.Set("iss", "https://"+s.config.Hostname) q.Set("code", code)
hashOrQuestion := "?" if authReq.Parameters.ResponseMode != nil { switch *authReq.Parameters.ResponseMode { case "fragment": hashOrQuestion = "#" case "query": // do nothing break default: if authReq.Parameters.ResponseType != "code" { hashOrQuestion = "#" } } } else { if authReq.Parameters.ResponseType != "code" { hashOrQuestion = "#" } }
return e.Redirect(303, authReq.Parameters.RedirectURI+hashOrQuestion+q.Encode())}