github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/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  }