github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/bitwarden/oauth.go (about) 1 package bitwarden 2 3 import ( 4 "strconv" 5 "strings" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/bitwarden/settings" 9 "github.com/cozy/cozy-stack/model/instance" 10 "github.com/cozy/cozy-stack/model/oauth" 11 "github.com/cozy/cozy-stack/model/permission" 12 "github.com/cozy/cozy-stack/pkg/consts" 13 "github.com/cozy/cozy-stack/pkg/crypto" 14 "github.com/golang-jwt/jwt/v5" 15 ) 16 17 // BitwardenScope was the OAuth scope, hard-coded with the doctypes needed by 18 // the Bitwarden apps. The new scope is dynamic, taken from the cozy-pass web 19 // manifest. 20 var BitwardenScope = strings.Join([]string{ 21 consts.BitwardenProfiles, 22 consts.BitwardenCiphers, 23 consts.BitwardenFolders, 24 consts.BitwardenOrganizations, 25 consts.BitwardenContacts, 26 consts.Konnectors, 27 consts.AppsSuggestion, 28 consts.Support, 29 }, " ") 30 31 // IsBitwardenClient returns true if the client can use the bitwarden refresh 32 // endpoint. 33 func IsBitwardenClient(client *oauth.Client, scope string) bool { 34 // Help the transition from hard-coded scope 35 if scope == BitwardenScope { 36 return true 37 } 38 39 return oauth.GetLinkedAppSlug(client.SoftwareID) == consts.PassSlug 40 } 41 42 // ParseBitwardenDeviceType takes a deviceType (Bitwarden) and transforms it 43 // into a client_kind (Cozy). 44 // See https://github.com/bitwarden/server/blob/f37f33512046707eef69a2cb3944338de819439d/src/Core/Enums/DeviceType.cs 45 func ParseBitwardenDeviceType(deviceType string) string { 46 device, err := strconv.Atoi(deviceType) 47 if err == nil { 48 switch device { 49 case 0, 1, 15, 16: 50 // 0 = Android 51 // 1 = iOS 52 // 15 = Android (amazon variant) 53 // 16 = UWP 54 return "mobile" 55 case 6, 7, 8: 56 // 6 = Windows 57 // 7 = macOS 58 // 8 = Linux 59 return "desktop" 60 case 2, 3, 4, 5, 19, 20: 61 // 2 = Chrome extension 62 // 3 = Firefox extension 63 // 4 = Opera extension 64 // 5 = Edge extension 65 // 19 = Vivaldi extension 66 // 20 = Safari extension 67 return "browser" 68 case 9, 10, 11, 12, 13, 14, 17, 18: 69 // 9 = Chrome 70 // 10 = Firefox 71 // 11 = Opera 72 // 12 = Edge 73 // 13 = Internet Explorer 74 // 14 = Unknown browser 75 // 17 = Safari 76 // 18 = Vivaldi 77 return "web" 78 } 79 } 80 return "unknown" 81 } 82 83 // CreateAccessJWT returns a new JSON Web Token that can be used with Bitwarden 84 // apps. It is an access token, with some additional custom fields. 85 // See https://github.com/bitwarden/jslib/blob/master/common/src/services/token.service.ts 86 func CreateAccessJWT(i *instance.Instance, c *oauth.Client) (string, error) { 87 now := time.Now() 88 name, err := i.SettingsPublicName() 89 if err != nil || name == "" { 90 name = "Anonymous" 91 } 92 var stamp string 93 if settings, err := settings.Get(i); err == nil { 94 stamp = settings.SecurityStamp 95 } 96 scope := BitwardenScope 97 if slug := oauth.GetLinkedAppSlug(c.SoftwareID); slug != "" { 98 scope = oauth.BuildLinkedAppScope(slug) 99 } 100 token, err := crypto.NewJWT(i.OAuthSecret, permission.BitwardenClaims{ 101 Claims: permission.Claims{ 102 RegisteredClaims: jwt.RegisteredClaims{ 103 Audience: jwt.ClaimStrings{consts.AccessTokenAudience}, 104 Issuer: i.Domain, 105 NotBefore: jwt.NewNumericDate(now.Add(-60 * time.Second)), 106 IssuedAt: jwt.NewNumericDate(now), 107 ExpiresAt: jwt.NewNumericDate(now.Add(consts.AccessTokenValidityDuration)), 108 Subject: i.ID(), 109 }, 110 SStamp: stamp, 111 Scope: scope, 112 }, 113 ClientID: c.CouchID, 114 Name: name, 115 Email: string(i.PassphraseSalt()), 116 Verified: false, 117 Premium: false, 118 }) 119 if err != nil { 120 i.Logger().WithNamespace("oauth"). 121 Errorf("Failed to create the bitwarden access token: %s", err) 122 } 123 return token, err 124 } 125 126 // CreateRefreshJWT returns a new JSON Web Token that can be used with 127 // Bitwarden apps. It is a refresh token, with an additional security stamp. 128 func CreateRefreshJWT(i *instance.Instance, c *oauth.Client) (string, error) { 129 var stamp string 130 if settings, err := settings.Get(i); err == nil { 131 stamp = settings.SecurityStamp 132 } 133 scope := BitwardenScope 134 if slug := oauth.GetLinkedAppSlug(c.SoftwareID); slug != "" { 135 scope = oauth.BuildLinkedAppScope(slug) 136 } 137 token, err := crypto.NewJWT(i.OAuthSecret, permission.Claims{ 138 RegisteredClaims: jwt.RegisteredClaims{ 139 Audience: jwt.ClaimStrings{consts.RefreshTokenAudience}, 140 Issuer: i.Domain, 141 IssuedAt: jwt.NewNumericDate(time.Now()), 142 Subject: c.CouchID, 143 }, 144 SStamp: stamp, 145 Scope: scope, 146 }) 147 if err != nil { 148 i.Logger().WithNamespace("oauth"). 149 Errorf("Failed to create the bitwarden refresh token: %s", err) 150 } 151 return token, err 152 }