Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/templates/sharing_discovery.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ <h1 class="h4 h2-md mb-2 text-center">{{t "Sharing Connect to Cozy"}}</h1>
</div>

<footer class="w-100 d-flex flex-column flex-md-column-reverse">
{{if .OIDCLink}}
<a href="{{.OIDCLink}}">{{t "Sharing Connect with OIDC"}}</a>
{{end}}
<p class="text-center mb-3">
{{t "Sharing No Cozy"}}
<a href="{{t "Sharing Discover Cozy URL"}}">
Expand Down
85 changes: 85 additions & 0 deletions web/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/cozy/cozy-stack/model/instance/lifecycle"
"github.com/cozy/cozy-stack/model/oauth"
"github.com/cozy/cozy-stack/model/session"
"github.com/cozy/cozy-stack/model/sharing"
build "github.com/cozy/cozy-stack/pkg/config"
"github.com/cozy/cozy-stack/pkg/config/config"
"github.com/cozy/cozy-stack/pkg/consts"
Expand Down Expand Up @@ -73,6 +74,21 @@ func StartFranceConnect(c echo.Context) error {
return c.Redirect(http.StatusSeeOther, u)
}

// Sharing is the route to use the SSO to accept a shared drive.
func Sharing(c echo.Context) error {
inst := middlewares.GetInstance(c)
conf, err := getGenericConfig(inst.ContextName)
if err != nil {
inst.Logger().WithNamespace("oidc").Infof("Start error: %s", err)
return renderError(c, nil, http.StatusNotFound, "Sorry, the context was not found.")
}
u, err := makeSharingStartURL(inst.Domain, c.QueryParam("sharingID"), c.QueryParam("state"), conf)
if err != nil {
return renderError(c, nil, http.StatusNotFound, "Sorry, the server is not configured for OpenID Connect.")
}
return c.Redirect(http.StatusSeeOther, u)
}

func checkRedirectURIForBitwarden(redirectURI string) error {
if redirectURI == "" {
return errors.New("redirect URI is required")
Expand Down Expand Up @@ -285,6 +301,9 @@ func Login(c echo.Context) error {
logger.WithNamespace("oidc").Errorf("Error on getToken: %s", err)
return renderError(c, inst, http.StatusBadGateway, "Error from the identity provider.")
}
if state.SharingID != "" {
return acceptSharing(c, inst, conf, token, state)
}
}

// Check 2FA if enabled, and if yes, render an HTML page to check if
Expand All @@ -309,6 +328,48 @@ func Login(c echo.Context) error {
return createSessionAndRedirect(c, inst, redirect, confirm, sid)
}

func acceptSharing(c echo.Context, ownerInst *instance.Instance, conf *Config, token string, state *stateHolder) error {
params, err := getUserInfo(conf, token)
if err != nil {
return err
}

sub, ok := params["sub"].(string)
if !ok {
logger.WithNamespace("oidc").Errorf("Missing sub")
return ErrAuthenticationFailed
}
var memberInst *instance.Instance
domain, err := extractDomain(conf, params)
if err == nil {
memberInst, err = lifecycle.GetInstance(domain)
} else if err == ErrAuthenticationFailed && conf.AllowCustomInstance {
memberInst, err = findInstanceBySub(sub, state.BitwardenContext)
}
if err != nil {
return renderError(c, nil, http.StatusNotFound, "Sorry, the cozy was not found.")
}

s, err := sharing.FindSharing(ownerInst, state.SharingID)
if err != nil {
return err
}
member, err := s.FindMemberByState(state.SharingState)
if err != nil {
return err
}
memberURL := memberInst.PageURL("/", nil)
if err = s.RegisterCozyURL(ownerInst, member, memberURL); err != nil {
return err
}
sharing.PersistInstanceURL(ownerInst, member.Email, member.Instance)
redirectURL, err := member.GenerateOAuthURL(s, "")
if err != nil {
return err
}
return c.Redirect(http.StatusFound, redirectURL)
}

func TwoFactor(c echo.Context) error {
accessToken := c.FormValue("access-token")
redirect := c.FormValue("redirect")
Expand Down Expand Up @@ -746,6 +807,29 @@ func makeStartURL(domain, redirect, confirm, bitwardenContext string, conf *Conf
return u.String(), nil
}

func makeSharingStartURL(domain, sharingID, sharingState string, conf *Config) (string, error) {
u, err := url.Parse(conf.AuthorizeURL)
if err != nil {
return "", err
}
state := newSharingStateHolder(domain, sharingID, sharingState)
if err = getStorage().Add(state); err != nil {
return "", err
}
vv := u.Query()
vv.Add("response_type", "code")
vv.Add("scope", conf.Scope)
vv.Add("client_id", conf.ClientID)
vv.Add("redirect_uri", conf.RedirectURI)
vv.Add("state", state.id)
vv.Add("nonce", state.Nonce)
if conf.Provider == FranceConnectProvider {
vv.Add("acr_values", "eidas1")
}
u.RawQuery = vv.Encode()
return u.String(), nil
}

var oidcClient = &http.Client{
Timeout: 15 * time.Second,
}
Expand Down Expand Up @@ -1066,6 +1150,7 @@ func renderError(c echo.Context, inst *instance.Instance, code int, msg string)
func Routes(router *echo.Group) {
router.GET("/start", Start, middlewares.NeedInstance, middlewares.CheckOnboardingNotFinished)
router.GET("/franceconnect", StartFranceConnect, middlewares.NeedInstance, middlewares.CheckOnboardingNotFinished)
router.GET("/sharing", Sharing, middlewares.NeedInstance)
router.GET("/bitwarden/:context", BitwardenStart)
router.POST("/bitwarden/:context", BitwardenExchange, middlewares.NeedInstance)
router.GET("/redirect", Redirect)
Expand Down
15 changes: 15 additions & 0 deletions web/oidc/statestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type stateHolder struct {
Nonce string
Confirm string
BitwardenContext string
SharingID string
SharingState string
}

type ProviderOIDC int
Expand All @@ -50,6 +52,19 @@ func newStateHolder(domain, redirect, confirm, bitwardenContext string, provider
}
}

func newSharingStateHolder(domain, sharingID, sharingState string) *stateHolder {
id := hex.EncodeToString(crypto.GenerateRandomBytes(24))
nonce := hex.EncodeToString(crypto.GenerateRandomBytes(24))
return &stateHolder{
id: id,
Provider: GenericProvider,
Instance: domain,
Nonce: nonce,
SharingID: sharingID,
SharingState: sharingState,
}
}

type stateStorage interface {
Add(*stateHolder) error
Find(id string) *stateHolder
Expand Down
11 changes: 11 additions & 0 deletions web/sharings/sharings.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,16 @@ func renderDiscoveryForm(c echo.Context, inst *instance.Instance, code int, shar
} else if parts := strings.SplitN(fqdn, ".", 2); len(parts) == 2 {
slug, domain = parts[0], parts[1]
}
oidcLink := ""
if oidc, ok := config.GetOIDC(inst.ContextName); ok {
if clientID, _ := oidc["client_id"].(string); clientID != "" {
q := url.Values{
"sharingID": {sharingID},
"state": {state},
}
oidcLink = inst.PageURL("/oidc/sharing", q)
}
}
return c.Render(code, "sharing_discovery.html", echo.Map{
"Domain": inst.ContextualDomain(),
"ContextName": inst.ContextName,
Expand All @@ -665,6 +675,7 @@ func renderDiscoveryForm(c echo.Context, inst *instance.Instance, code int, shar
"State": state,
"ShareCode": sharecode,
"Shortcut": shortcut,
"OIDCLink": oidcLink,
"URLError": code == http.StatusBadRequest,
"NotEmailError": code == http.StatusPreconditionFailed,
})
Expand Down
41 changes: 21 additions & 20 deletions web/statik/statik.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading