package auth import ( "bytes" "fmt" "html/template" "io" "net/http" "path/filepath" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db" "github.com/google/uuid" "go.uber.org/zap" ) const ApprovalRoute = "/approval" var scopeDescriptions = map[string]string{ "offline_access": "Have offline access", "profile": "View basic profile information", "email": "View your email address", "groups": "View your groups", } func scopeDescription(rawScope string) string { if desc, ok := scopeDescriptions[rawScope]; ok { return desc } return rawScope } type approvalData struct { Scopes []string Client string AuthReqID string } type ApprovalController struct { l *zap.SugaredLogger st db.Storage baseDir string } func NewApprovalController(l *zap.SugaredLogger, st db.Storage, baseDir string) *ApprovalController { return &ApprovalController{ l: l, st: st, baseDir: baseDir, } } func (c *ApprovalController) handleFormResponse(w http.ResponseWriter, r *http.Request) { reqID, err := uuid.Parse(r.Form.Get("req")) if err != nil { c.l.Errorf("Invalid request ID: %s", err) helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("invalid query format"), c.l) return } if r.Form.Get("approval") != "approve" { c.l.Debug("Approval rejected") helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("approval rejected"), c.l) return } if err := c.st.AuthRequestStorage().GiveConsent(r.Context(), reqID); err != nil { c.l.Errorf("Failed to approve request: %s", err) helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l) return } http.Redirect(w, r, fmt.Sprintf("/callback?code=%s&state=%s", r.Form.Get("code"), reqID.String()), http.StatusSeeOther) } func (c *ApprovalController) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { c.l.Errorf("Failed to parse query: %s", err) helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("invalid query format"), c.l) return } if r.Method == http.MethodPost { c.handleFormResponse(w, r) return } state := r.Form.Get("state") reqID, err := uuid.Parse(state) if err != nil { c.l.Errorf("Invalid state %q: %s", state, err) helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("unexpected state"), c.l) return } req, err := c.st.AuthRequestStorage().GetAuthRequestByID(r.Context(), reqID) if err != nil { c.l.Errorf("Failed to get auth request from DB: %s", err) helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l) return } app, err := c.st.ClientStorage().GetClientByID(r.Context(), req.ClientID) if err != nil { c.l.Errorf("Failed to get client details from DB: %s", err) helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l) return } data := approvalData{ Scopes: []string{}, Client: app.Name, AuthReqID: reqID.String(), } for _, s := range req.Scopes { if s == "openid" { // it's implied we want that, no consent is really important there continue } data.Scopes = append(data.Scopes, scopeDescription(s)) } lp := filepath.Join(c.baseDir, "templates", "approval.html") hdrTpl := filepath.Join(c.baseDir, "templates", "header.html") footTpl := filepath.Join(c.baseDir, "templates", "footer.html") tmpl, err := template.New("approval.html").ParseFiles(hdrTpl, footTpl, lp) if err != nil { c.l.Errorf("Failed to parse templates: %s", err) helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l) return } buf := new(bytes.Buffer) if err := tmpl.Execute(buf, data); err != nil { c.l.Errorf("Failed to execute template: %s", err) helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l) return } _, err = io.Copy(w, buf) if err != nil { c.l.Errorf("Failed to write response: %s", err) helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l) return } }