Skip to content

Commit

Permalink
Make the bitwarden OAuth clients linked to cozy-pass web
Browse files Browse the repository at this point in the history
  • Loading branch information
nono committed Feb 22, 2024
1 parent c526996 commit 72478fd
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 33 deletions.
57 changes: 27 additions & 30 deletions model/bitwarden/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import (
"github.com/golang-jwt/jwt/v5"
)

// BitwardenScope is the OAuth scope, and it is hard-coded with the doctypes
// needed by the Bitwarden apps.
// BitwardenScope was the OAuth scope, hard-coded with the doctypes needed by
// the Bitwarden apps. The new scope is dynamic, taken from the cozy-pass web
// manifest.
var BitwardenScope = strings.Join([]string{
consts.BitwardenProfiles,
consts.BitwardenCiphers,
Expand All @@ -27,33 +28,21 @@ var BitwardenScope = strings.Join([]string{
consts.Support,
}, " ")

// oldBitwardenScope is here to help the transition of bitwarden tokens, as the
// com.bitwarden.contacts doctype has been added to the bitwarden scope.
var oldBitwardenScope = strings.Join([]string{
consts.BitwardenProfiles,
consts.BitwardenCiphers,
consts.BitwardenFolders,
consts.BitwardenOrganizations,
consts.Konnectors,
consts.AppsSuggestion,
consts.Support,
}, " ")

// IsBitwardenScope returns true if it is the right scope for refreshing a
// bitwarden token.
func IsBitwardenScope(scope string) bool {
switch scope {
case BitwardenScope, oldBitwardenScope:
// IsBitwardenClient returns true if the client can use the bitwarden refresh
// endpoint.
func IsBitwardenClient(client *oauth.Client, scope string) bool {
// Help the transition from hard-coded scope
if scope == BitwardenScope {
return true
default:
return false
}

return oauth.GetLinkedAppSlug(client.SoftwareID) == consts.PassSlug
}

// ParseBitwardenDeviceType takes a deviceType (Bitwarden) and transforms it
// into a client_kind and a software_id (Cozy).
// into a client_kind (Cozy).
// See https://github.com/bitwarden/server/blob/f37f33512046707eef69a2cb3944338de819439d/src/Core/Enums/DeviceType.cs
func ParseBitwardenDeviceType(deviceType string) (string, string) {
func ParseBitwardenDeviceType(deviceType string) string {
device, err := strconv.Atoi(deviceType)
if err == nil {
switch device {
Expand All @@ -62,20 +51,20 @@ func ParseBitwardenDeviceType(deviceType string) (string, string) {
// 1 = iOS
// 15 = Android (amazon variant)
// 16 = UWP
return "mobile", "github.com/bitwarden/mobile"
return "mobile"
case 6, 7, 8:
// 6 = Windows
// 7 = macOS
// 8 = Linux
return "desktop", "github.com/bitwarden/desktop"
return "desktop"
case 2, 3, 4, 5, 19, 20:
// 2 = Chrome extension
// 3 = Firefox extension
// 4 = Opera extension
// 5 = Edge extension
// 19 = Vivaldi extension
// 20 = Safari extension
return "browser", "github.com/bitwarden/browser"
return "browser"
case 9, 10, 11, 12, 13, 14, 17, 18:
// 9 = Chrome
// 10 = Firefox
Expand All @@ -85,10 +74,10 @@ func ParseBitwardenDeviceType(deviceType string) (string, string) {
// 14 = Unknown browser
// 17 = Safari
// 18 = Vivaldi
return "web", "github.com/bitwarden/web"
return "web"
}
}
return "unknown", "github.com/bitwarden"
return "unknown"
}

// CreateAccessJWT returns a new JSON Web Token that can be used with Bitwarden
Expand All @@ -104,6 +93,10 @@ func CreateAccessJWT(i *instance.Instance, c *oauth.Client) (string, error) {
if settings, err := settings.Get(i); err == nil {
stamp = settings.SecurityStamp
}
scope := BitwardenScope
if slug := oauth.GetLinkedAppSlug(c.SoftwareID); slug != "" {
scope = oauth.BuildLinkedAppScope(slug)
}
token, err := crypto.NewJWT(i.OAuthSecret, permission.BitwardenClaims{
Claims: permission.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Expand All @@ -115,7 +108,7 @@ func CreateAccessJWT(i *instance.Instance, c *oauth.Client) (string, error) {
Subject: i.ID(),
},
SStamp: stamp,
Scope: BitwardenScope,
Scope: scope,
},
ClientID: c.CouchID,
Name: name,
Expand All @@ -137,6 +130,10 @@ func CreateRefreshJWT(i *instance.Instance, c *oauth.Client) (string, error) {
if settings, err := settings.Get(i); err == nil {
stamp = settings.SecurityStamp
}
scope := BitwardenScope
if slug := oauth.GetLinkedAppSlug(c.SoftwareID); slug != "" {
scope = oauth.BuildLinkedAppScope(slug)
}
token, err := crypto.NewJWT(i.OAuthSecret, permission.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{consts.RefreshTokenAudience},
Expand All @@ -145,7 +142,7 @@ func CreateRefreshJWT(i *instance.Instance, c *oauth.Client) (string, error) {
Subject: c.CouchID,
},
SStamp: stamp,
Scope: BitwardenScope,
Scope: scope,
})
if err != nil {
i.Logger().WithNamespace("oauth").
Expand Down
5 changes: 5 additions & 0 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package consts is only for declaring some constants used by the stack, like
// some slugs, IDs, doctypes, etc.
package consts

const (
Expand All @@ -16,6 +18,9 @@ const (
// referencing a directory that contains the notes with collaborative
// edition.
NotesSlug = "notes"
// PassSlug is the slug of cozy-pass webapp, which is used by the stack for
// linking the bitwarden OAuth clients.
PassSlug = "passwords"
)

const (
Expand Down
11 changes: 8 additions & 3 deletions web/bitwarden/bitwarden.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func getInitialCredentials(c echo.Context) error {
}

// Register the client
kind, softwareID := bitwarden.ParseBitwardenDeviceType(c.FormValue("deviceType"))
kind := bitwarden.ParseBitwardenDeviceType(c.FormValue("deviceType"))
clientName := c.FormValue("clientName")
if clientName == "" {
clientName = "Bitwarden " + c.FormValue("deviceName")
Expand All @@ -253,7 +253,7 @@ func getInitialCredentials(c echo.Context) error {
RedirectURIs: []string{"https://cozy.io/"},
ClientName: clientName,
ClientKind: kind,
SoftwareID: softwareID,
SoftwareID: "registry://" + consts.PassSlug,
}
if err := client.Create(inst, oauth.NotPending); err != nil {
return c.JSON(err.Code, err)
Expand Down Expand Up @@ -407,7 +407,7 @@ func refreshToken(c echo.Context) error {

// Check the refresh token
claims, ok := oauth.ValidTokenWithSStamp(inst, consts.RefreshTokenAudience, refresh)
if !ok || !bitwarden.IsBitwardenScope(claims.Scope) {
if !ok {
return c.JSON(http.StatusBadRequest, echo.Map{
"error": "invalid refresh token",
})
Expand All @@ -423,6 +423,11 @@ func refreshToken(c echo.Context) error {
"error": "the client must be registered",
})
}
if !bitwarden.IsBitwardenClient(client, claims.Scope) {
return c.JSON(http.StatusBadRequest, echo.Map{
"error": "invalid refresh token",
})
}

// Create the credentials
access, err := bitwarden.CreateAccessJWT(inst, client)
Expand Down
16 changes: 16 additions & 0 deletions web/bitwarden/bitwarden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"
"time"

"github.com/cozy/cozy-stack/model/app"
"github.com/cozy/cozy-stack/model/bitwarden"
"github.com/cozy/cozy-stack/model/bitwarden/settings"
"github.com/cozy/cozy-stack/model/instance/lifecycle"
Expand Down Expand Up @@ -45,6 +46,21 @@ func TestBitwarden(t *testing.T) {
ts := setup.GetTestServer("/bitwarden", Routes)
ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler

// Install cozy-pass webapp (required for OAuth linked clients)
installer, err := app.NewInstaller(inst, app.Copier(consts.WebappType, inst),
&app.InstallerOptions{
Operation: app.Install,
Type: consts.WebappType,
Slug: "passwords",
SourceURL: "registry://passwords",
Registries: inst.Registries(),
},
)
require.NoError(t, err)

_, err = installer.RunSync()
require.NoError(t, err)

t.Run("Prelogin", func(t *testing.T) {
e := testutils.CreateTestClient(t, ts.URL)

Expand Down

0 comments on commit 72478fd

Please sign in to comment.