github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/oauth.go (about) 1 package sharing 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "net/http" 8 "net/url" 9 "strings" 10 "time" 11 12 "github.com/cozy/cozy-stack/client/auth" 13 "github.com/cozy/cozy-stack/client/request" 14 "github.com/cozy/cozy-stack/model/bitwarden/settings" 15 "github.com/cozy/cozy-stack/model/contact" 16 "github.com/cozy/cozy-stack/model/instance" 17 "github.com/cozy/cozy-stack/model/oauth" 18 "github.com/cozy/cozy-stack/model/permission" 19 csettings "github.com/cozy/cozy-stack/model/settings" 20 "github.com/cozy/cozy-stack/model/vfs" 21 "github.com/cozy/cozy-stack/pkg/consts" 22 "github.com/cozy/cozy-stack/pkg/couchdb" 23 "github.com/cozy/cozy-stack/pkg/couchdb/mango" 24 "github.com/cozy/cozy-stack/pkg/jsonapi" 25 "github.com/cozy/cozy-stack/pkg/safehttp" 26 jwt "github.com/golang-jwt/jwt/v5" 27 "github.com/labstack/echo/v4" 28 ) 29 30 // CreateSharingRequest sends information about the sharing to the recipient's cozy 31 func (m *Member) CreateSharingRequest(inst *instance.Instance, s *Sharing, c *Credentials, u *url.URL) error { 32 if len(c.XorKey) == 0 { 33 return ErrInvalidSharing 34 } 35 36 rules := make([]Rule, 0, len(s.Rules)) 37 for _, rule := range s.Rules { 38 if rule.Local { 39 continue 40 } 41 if rule.FilesByID() && len(rule.Values) > 0 { 42 if fileDoc, err := inst.VFS().FileByID(rule.Values[0]); err == nil { 43 // err != nil means that the target is a directory and not 44 // a file, and we leave the mime blank in that case. 45 rule.Mime = fileDoc.Mime 46 } 47 } 48 values := make([]string, len(rule.Values)) 49 for i, v := range rule.Values { 50 switch rule.Selector { 51 case "", "id", "_id", "organization_id": 52 values[i] = XorID(v, c.XorKey) 53 case couchdb.SelectorReferencedBy: 54 parts := strings.SplitN(v, "/", 2) 55 values[i] = parts[0] + "/" + XorID(parts[1], c.XorKey) 56 default: 57 values[i] = v 58 } 59 } 60 rule.Values = values 61 rules = append(rules, rule) 62 } 63 members := make([]Member, len(s.Members)) 64 for i, m := range s.Members { 65 // Instance and name are private... 66 members[i] = Member{ 67 Status: m.Status, 68 PublicName: m.PublicName, 69 Email: m.Email, 70 ReadOnly: m.ReadOnly, 71 OnlyInGroups: m.OnlyInGroups, 72 Groups: m.Groups, 73 } 74 // ... except for the sharer and the recipient of this request 75 if i == 0 || &s.Credentials[i-1] == c { 76 members[i].Instance = m.Instance 77 } 78 } 79 sh := APISharing{ 80 &Sharing{ 81 SID: s.SID, 82 Active: false, 83 Owner: false, 84 Open: s.Open, 85 Description: s.Description, 86 AppSlug: s.AppSlug, 87 PreviewPath: s.PreviewPath, 88 CreatedAt: s.CreatedAt, 89 UpdatedAt: s.UpdatedAt, 90 Rules: rules, 91 Members: members, 92 Groups: s.Groups, 93 NbFiles: s.countFiles(inst), 94 }, 95 nil, 96 nil, 97 } 98 data, err := jsonapi.MarshalObject(&sh) 99 if err != nil { 100 return err 101 } 102 body, err := json.Marshal(jsonapi.Document{Data: &data}) 103 if err != nil { 104 return err 105 } 106 opts := request.Options{ 107 Method: http.MethodPut, 108 Scheme: u.Scheme, 109 Domain: u.Host, 110 Path: "/sharings/" + s.SID, 111 Headers: request.Headers{ 112 echo.HeaderAccept: jsonapi.ContentType, 113 echo.HeaderContentType: jsonapi.ContentType, 114 }, 115 Queries: u.Query(), 116 Body: bytes.NewReader(body), 117 } 118 res, err := request.Req(&opts) 119 if res != nil && res.StatusCode == http.StatusConflict { 120 return ErrAlreadyAccepted 121 } 122 if err != nil { 123 return err 124 } 125 res.Body.Close() 126 return nil 127 } 128 129 // countFiles returns the number of files that should be uploaded on the 130 // initial synchronisation. 131 func (s *Sharing) countFiles(inst *instance.Instance) int { 132 count := 0 133 for _, rule := range s.Rules { 134 if rule.DocType != consts.Files || rule.Local || len(rule.Values) == 0 { 135 continue 136 } 137 138 if rule.Selector == "" || rule.Selector == "id" { 139 fs := inst.VFS() 140 for _, fileID := range rule.Values { 141 dir, _, err := fs.DirOrFileByID(fileID) 142 if err != nil { 143 continue 144 } 145 if dir != nil { 146 nb, err := countFilesInDirectory(inst, dir) 147 if err != nil { 148 continue 149 } 150 count += nb 151 } else { 152 count++ 153 } 154 } 155 } else { 156 var resCount couchdb.ViewResponse 157 for _, val := range rule.Values { 158 reqCount := &couchdb.ViewRequest{Key: val, Reduce: true} 159 err := couchdb.ExecView(inst, couchdb.FilesReferencedByView, reqCount, &resCount) 160 if err == nil && len(resCount.Rows) > 0 { 161 count += int(resCount.Rows[0].Value.(float64)) 162 } 163 } 164 } 165 } 166 return count 167 } 168 169 func countFilesInDirectory(inst *instance.Instance, dir *vfs.DirDoc) (int, error) { 170 // Find the subdirectories 171 start := dir.Fullpath + "/" 172 stop := dir.Fullpath + "0" // 0 is the next ascii character after / 173 if dir.DocID == consts.RootDirID { 174 start = "/" 175 stop = "0" 176 } 177 sel := mango.And( 178 mango.Gt("path", start), 179 mango.Lt("path", stop), 180 mango.Equal("type", consts.DirType), 181 ) 182 req := &couchdb.FindRequest{ 183 UseIndex: "dir-by-path", 184 Selector: sel, 185 Fields: []string{"_id"}, 186 Limit: 10000, 187 } 188 var children []couchdb.JSONDoc 189 err := couchdb.FindDocs(inst, consts.Files, req, &children) 190 if err != nil { 191 return 0, err 192 } 193 keys := make([]interface{}, len(children)+1) 194 keys[0] = dir.DocID 195 for i, child := range children { 196 keys[i+1] = child.ID() 197 } 198 199 // Get the number of files for the directory and each of its sub-directory 200 var resp couchdb.ViewResponse 201 err = couchdb.ExecView(inst, couchdb.DiskUsageView, &couchdb.ViewRequest{ 202 Keys: keys, 203 Limit: 100_000, 204 }, &resp) 205 if err != nil { 206 return 0, err 207 } 208 return len(resp.Rows), nil 209 } 210 211 // RegisterCozyURL saves a new Cozy URL for a member 212 func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL string) error { 213 if !s.Owner { 214 return ErrInvalidSharing 215 } 216 if m.Status == MemberStatusReady { 217 return ErrAlreadyAccepted 218 } 219 if m.Status == MemberStatusOwner || m.Status == MemberStatusRevoked { 220 return ErrMemberNotFound 221 } 222 223 cozyURL = strings.TrimSpace(cozyURL) 224 if !strings.Contains(cozyURL, "://") { 225 cozyURL = "https://" + cozyURL 226 } 227 u, err := url.Parse(cozyURL) 228 if err != nil || u.Host == "" { 229 return ErrInvalidURL 230 } 231 u.Path = "" 232 u.RawPath = "" 233 u.RawQuery = "" 234 u.Fragment = "" 235 m.Instance = u.String() 236 237 creds := s.FindCredentials(m) 238 if creds == nil { 239 return ErrInvalidSharing 240 } 241 if err = m.CreateSharingRequest(inst, s, creds, u); err != nil { 242 inst.Logger().WithNamespace("sharing").Warnf("Error on sharing request: %s", err) 243 if errors.Is(err, ErrAlreadyAccepted) { 244 return err 245 } 246 return ErrRequestFailed 247 } 248 return couchdb.UpdateDoc(inst, s) 249 } 250 251 // GenerateOAuthURL takes care of creating a correct OAuth request for 252 // the given member of the sharing. 253 func (m *Member) GenerateOAuthURL(s *Sharing) (string, error) { 254 if !s.Owner || len(s.Members) != len(s.Credentials)+1 { 255 return "", ErrInvalidSharing 256 } 257 creds := s.FindCredentials(m) 258 if creds == nil { 259 return "", ErrInvalidSharing 260 } 261 if m.Instance == "" { 262 return "", ErrNoOAuthClient 263 } 264 265 u, err := url.Parse(m.Instance) 266 if err != nil { 267 return "", err 268 } 269 u.Path = "/auth/authorize/sharing" 270 271 q := url.Values{ 272 "sharing_id": {s.SID}, 273 "state": {creds.State}, 274 } 275 u.RawQuery = q.Encode() 276 277 return u.String(), nil 278 } 279 280 // CreateOAuthClient creates an OAuth client for a recipient of the given sharing 281 func CreateOAuthClient(inst *instance.Instance, m *Member) (*oauth.Client, error) { 282 if m.Instance == "" { 283 return nil, ErrInvalidURL 284 } 285 cli := oauth.Client{ 286 RedirectURIs: []string{m.Instance + "/sharings/answer"}, 287 ClientName: "Sharing " + m.PublicName, 288 ClientKind: "sharing", 289 SoftwareID: "github.com/cozy/cozy-stack", 290 ClientURI: m.Instance + "/", 291 } 292 if err := cli.Create(inst, oauth.NotPending); err != nil { 293 return nil, ErrInternalServerError 294 } 295 return &cli, nil 296 } 297 298 // DeleteOAuthClient removes the client associated to the given member 299 func DeleteOAuthClient(inst *instance.Instance, m *Member, cred *Credentials) error { 300 if m.Instance == "" { 301 return ErrInvalidURL 302 } 303 clientID := cred.InboundClientID 304 if clientID == "" { 305 return nil 306 } 307 client, err := oauth.FindClient(inst, clientID) 308 if err != nil { 309 if couchdb.IsNotFoundError(err) { 310 return nil 311 } 312 return err 313 } 314 if cerr := client.Delete(inst); cerr != nil { 315 return errors.New(cerr.Error) 316 } 317 return nil 318 } 319 320 // ConvertOAuthClient converts an OAuth client from one type 321 // (model/oauth.Client) to another (client/auth.Client) 322 func ConvertOAuthClient(c *oauth.Client) *auth.Client { 323 return &auth.Client{ 324 ClientID: c.ClientID, 325 ClientSecret: c.ClientSecret, 326 SecretExpiresAt: c.SecretExpiresAt, 327 RegistrationToken: c.RegistrationToken, 328 RedirectURIs: c.RedirectURIs, 329 ClientName: c.ClientName, 330 ClientKind: c.ClientKind, 331 ClientURI: c.ClientURI, 332 LogoURI: c.LogoURI, 333 PolicyURI: c.PolicyURI, 334 SoftwareID: c.SoftwareID, 335 SoftwareVersion: c.SoftwareVersion, 336 } 337 } 338 339 // CreateAccessToken creates an access token for the given OAuth client, 340 // with a scope on this sharing. 341 func CreateAccessToken(inst *instance.Instance, cli *oauth.Client, sharingID string, verb permission.VerbSet) (*auth.AccessToken, error) { 342 scope := consts.Sharings + ":" + verb.String() + ":" + sharingID 343 cli.CouchID = cli.ClientID // XXX CouchID is required by CreateJWT 344 refresh, err := cli.CreateJWT(inst, consts.RefreshTokenAudience, scope) 345 if err != nil { 346 return nil, err 347 } 348 access, err := cli.CreateJWT(inst, consts.AccessTokenAudience, scope) 349 if err != nil { 350 return nil, err 351 } 352 return &auth.AccessToken{ 353 TokenType: "bearer", 354 AccessToken: access, 355 RefreshToken: refresh, 356 Scope: scope, 357 }, nil 358 } 359 360 // SendAnswer says to the sharer's Cozy that the sharing has been accepted, and 361 // materialize that by an exchange of credentials. 362 func (s *Sharing) SendAnswer(inst *instance.Instance, state string) error { 363 if s.Owner || len(s.Members) < 2 || len(s.Credentials) != 1 { 364 return ErrInvalidSharing 365 } 366 u, err := url.Parse(s.Members[0].Instance) 367 if s.Members[0].Instance == "" || err != nil { 368 return ErrInvalidSharing 369 } 370 cli, err := CreateOAuthClient(inst, &s.Members[0]) 371 if err != nil { 372 return err 373 } 374 token, err := CreateAccessToken(inst, cli, s.SID, permission.ALL) 375 if err != nil { 376 return err 377 } 378 name, err := csettings.PublicName(inst) 379 if err != nil { 380 inst.Logger().WithNamespace("sharing"). 381 Infof("No name for instance %v", inst) 382 } 383 ac := APICredentials{ 384 Credentials: &Credentials{ 385 State: state, 386 Client: ConvertOAuthClient(cli), 387 AccessToken: token, 388 }, 389 PublicName: name, 390 CID: s.SID, 391 } 392 if s.FirstBitwardenOrganizationRule() != nil { 393 setting, err := settings.Get(inst) 394 if err != nil { 395 return err 396 } 397 ac.Bitwarden = &APIBitwarden{ 398 UserID: inst.ID(), 399 PublicKey: setting.PublicKey, 400 } 401 } 402 data, err := jsonapi.MarshalObject(&ac) 403 if err != nil { 404 return err 405 } 406 body, err := json.Marshal(jsonapi.Document{Data: &data}) 407 if err != nil { 408 return err 409 } 410 res, err := request.Req(&request.Options{ 411 Method: http.MethodPost, 412 Scheme: u.Scheme, 413 Domain: u.Host, 414 Path: "/sharings/" + s.SID + "/answer", 415 Headers: request.Headers{ 416 echo.HeaderAccept: jsonapi.ContentType, 417 echo.HeaderContentType: jsonapi.ContentType, 418 }, 419 Body: bytes.NewReader(body), 420 }) 421 if err != nil { 422 return err 423 } 424 defer res.Body.Close() 425 426 for i, m := range s.Members { 427 if i > 0 && m.Instance != "" { 428 if m.Status == MemberStatusMailNotSent || 429 m.Status == MemberStatusPendingInvitation || 430 m.Status == MemberStatusSeen { 431 s.Members[i].Status = MemberStatusReady 432 } 433 } 434 } 435 436 if err = s.SetupReceiver(inst); err != nil { 437 return err 438 } 439 440 var creds Credentials 441 if _, err = jsonapi.Bind(res.Body, &creds); err != nil { 442 return ErrRequestFailed 443 } 444 s.Credentials[0].XorKey = creds.XorKey 445 s.Credentials[0].InboundClientID = cli.ClientID 446 s.Credentials[0].AccessToken = creds.AccessToken 447 s.Credentials[0].Client = creds.Client 448 s.Active = true 449 s.Initial = s.NbFiles > 0 450 return couchdb.UpdateDoc(inst, s) 451 } 452 453 // ProcessAnswer takes somes credentials and update the sharing with those. 454 func (s *Sharing) ProcessAnswer(inst *instance.Instance, creds *APICredentials) (*APICredentials, error) { 455 if !s.Owner || len(s.Members) != len(s.Credentials)+1 { 456 return nil, ErrInvalidSharing 457 } 458 for i, c := range s.Credentials { 459 if c.State == creds.State { 460 s.Members[i+1].Status = MemberStatusReady 461 s.Members[i+1].PublicName = creds.PublicName 462 s.Credentials[i].Client = creds.Client 463 s.Credentials[i].AccessToken = creds.AccessToken 464 ac := APICredentials{ 465 CID: s.SID, 466 Credentials: &Credentials{ 467 XorKey: c.XorKey, 468 }, 469 } 470 // Create the credentials for the recipient 471 cli, err := CreateOAuthClient(inst, &s.Members[i+1]) 472 if err != nil { 473 return &ac, nil 474 } 475 s.Credentials[i].InboundClientID = cli.ClientID 476 ac.Credentials.Client = ConvertOAuthClient(cli) 477 var verb permission.VerbSet 478 // In case of read-only, the recipient only needs read access on the 479 // sharing, e.g. to notify the sharer of a revocation 480 if s.ReadOnlyRules() || s.Members[i+1].ReadOnly { 481 verb = permission.Verbs(permission.GET) 482 } else { 483 verb = permission.ALL 484 } 485 token, err := CreateAccessToken(inst, cli, s.SID, verb) 486 if err != nil { 487 return &ac, nil 488 } 489 ac.Credentials.AccessToken = token 490 491 // Update the contact to fill the name if missing 492 if email := s.Members[i+1].Email; email != "" { 493 if c, err := contact.FindByEmail(inst, email); err == nil { 494 if err := c.AddNameIfMissing(inst, s.Members[i+1].PublicName, email); err != nil { 495 inst.Logger().WithNamespace("sharing"). 496 Warnf("Error on saving contact: %s", err) 497 } 498 } 499 } 500 501 s.Active = true 502 if err := couchdb.UpdateDoc(inst, s); err != nil { 503 if !couchdb.IsConflictError(err) { 504 return nil, err 505 } 506 // A conflict can occur when several users accept a sharing at 507 // the same time, and we should just retry in that case 508 s2, err2 := FindSharing(inst, s.SID) 509 if err2 != nil { 510 return nil, err 511 } 512 s2.Members[i+1] = s.Members[i+1] 513 s2.Credentials[i] = s.Credentials[i] 514 if err2 := couchdb.UpdateDoc(inst, s2); err2 != nil { 515 return nil, err 516 } 517 s = s2 518 } 519 if creds.Bitwarden != nil { 520 if err := s.SaveBitwarden(inst, &s.Members[i+1], creds.Bitwarden); err != nil { 521 return nil, err 522 } 523 } 524 go s.Setup(inst, &s.Members[i+1]) 525 return &ac, nil 526 } 527 } 528 return nil, ErrMemberNotFound 529 } 530 531 // ChangeOwnerAddress is used when the owner of the sharing has moved their 532 // instance to a new URL and the other members of the sharing are informed of 533 // the new URL. 534 func (s *Sharing) ChangeOwnerAddress(inst *instance.Instance, params APIMoved) error { 535 s.Members[0].Instance = params.NewInstance 536 s.Credentials[0].AccessToken.AccessToken = params.AccessToken 537 s.Credentials[0].AccessToken.RefreshToken = params.RefreshToken 538 updateContactAddress(inst, s.Members[0].Email, params.NewInstance) 539 return couchdb.UpdateDoc(inst, s) 540 } 541 542 // ChangeMemberAddress is used when a recipient of the sharing has moved their 543 // instance to a new URL and the owner if informed of the new URL. 544 func (s *Sharing) ChangeMemberAddress(inst *instance.Instance, m *Member, params APIMoved) error { 545 m.Instance = params.NewInstance 546 for i := range s.Members { 547 if i == 0 { 548 continue 549 } 550 if m.Same(s.Members[i]) { 551 s.Credentials[i-1].AccessToken.AccessToken = params.AccessToken 552 s.Credentials[i-1].AccessToken.RefreshToken = params.RefreshToken 553 } 554 } 555 updateContactAddress(inst, m.Email, params.NewInstance) 556 return couchdb.UpdateDoc(inst, s) 557 } 558 559 func updateContactAddress(inst *instance.Instance, email, newInstance string) { 560 if email == "" { 561 return 562 } 563 c, err := contact.FindByEmail(inst, email) 564 if err != nil { 565 return 566 } 567 _ = c.ChangeCozyURL(inst, newInstance) 568 } 569 570 // RefreshToken is used after a failed request with a 4xx error code. 571 // It checks if the targeted instance has moved, and tries on the new instance 572 // if it is the case. And, if needed, it renews the access token and retries 573 // the request. 574 func RefreshToken( 575 inst *instance.Instance, 576 reqErr error, 577 s *Sharing, 578 m *Member, 579 creds *Credentials, 580 opts *request.Options, 581 body []byte, 582 ) (*http.Response, error) { 583 if err, ok := reqErr.(*request.Error); ok && err.Status == http.StatusText(http.StatusGone) { 584 tryUpdateMemberInstance(err, m, opts) 585 } 586 587 if err := creds.Refresh(inst, s, m); err != nil { 588 return nil, err 589 } 590 opts.Headers["Authorization"] = "Bearer " + creds.AccessToken.AccessToken 591 if body != nil { 592 opts.Body = bytes.NewReader(body) 593 } 594 res, err := request.Req(opts) 595 if res != nil && res.StatusCode/100 == 5 { 596 return nil, ErrInternalServerError 597 } 598 return res, err 599 } 600 601 func tryUpdateMemberInstance(reqErr *request.Error, m *Member, opts *request.Options) { 602 m.Instance = reqErr.Title 603 u, err := url.Parse(m.Instance) 604 if err != nil { 605 return 606 } 607 opts.Scheme = u.Scheme 608 opts.Domain = u.Host 609 } 610 611 // ParseRequestError is used to parse an error in a request.Options, and it 612 // keeps the new instance URL when a Cozy has moved in Title. 613 func ParseRequestError(res *http.Response, body []byte) error { 614 if res.StatusCode != http.StatusGone { 615 return &request.Error{ 616 Status: http.StatusText(res.StatusCode), 617 Title: http.StatusText(res.StatusCode), 618 Detail: string(body), 619 } 620 } 621 622 var errors struct { 623 List jsonapi.ErrorList `json:"errors"` 624 } 625 if err := json.Unmarshal(body, &errors); err != nil { 626 return &request.Error{ 627 Status: http.StatusText(res.StatusCode), 628 Title: http.StatusText(res.StatusCode), 629 Detail: string(body), 630 } 631 } 632 var newInstance string 633 if len(errors.List) == 1 && errors.List[0].Links != nil && errors.List[0].Links.Related != "" { 634 newInstance = errors.List[0].Links.Related 635 } 636 return &request.Error{ 637 Status: http.StatusText(res.StatusCode), 638 Title: newInstance, 639 Detail: string(body), 640 } 641 } 642 643 // TryTokenForMovedSharing is used when a Cozy has been moved, and a sharing 644 // was not updated on the other Cozy for some reasons. When the other Cozy will 645 // try to make a request to the source Cozy, it will get a 410 Gone error. This 646 // error will also tell it the URL of the new Cozy. Thus, it can try to refresh 647 // the token on the destination Cozy. And, as the refresh token was emitted on 648 // the source Cozy (and not the target Cozy), we need to do some tricks to 649 // manage this refresh. This function is here for that. 650 func TryTokenForMovedSharing(i *instance.Instance, c *oauth.Client, token string) (string, permission.Claims, bool) { 651 // Extract the sharing ID from the scope of the refresh token 652 claims := permission.Claims{} 653 if token == "" { 654 return "", claims, false 655 } 656 _, _, err := new(jwt.Parser).ParseUnverified(token, &claims) 657 if err != nil { 658 return "", claims, false 659 } 660 parts := strings.Split(claims.Scope, ":") 661 if len(parts) != 3 || parts[0] != consts.Sharings { 662 return "", claims, false 663 } 664 665 // Find the sharing and check that it has been moved from another instance 666 s, err := FindSharing(i, parts[2]) 667 if err != nil || s.MovedFrom == "" { 668 return "", claims, false 669 } 670 validUntil := s.UpdatedAt.Add(consts.AccessTokenValidityDuration) 671 if validUntil.Before(time.Now().UTC()) { 672 // This trick is only accepted in the week following the move, not after 673 return "", claims, false 674 } 675 676 // Call the other instance and check the response 677 q := url.Values{ 678 "grant_type": {"refresh_token"}, 679 "refresh_token": {token}, 680 "client_id": {c.ClientID}, 681 "client_secret": {c.ClientSecret}, 682 } 683 if c.ClientID == "" { 684 q.Set("client_id", c.CouchID) 685 } 686 payload := strings.NewReader(q.Encode()) 687 req, err := http.NewRequest("POST", s.MovedFrom+"/auth/access_token", payload) 688 if err != nil { 689 return "", claims, false 690 } 691 req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) 692 req.Header.Add(echo.HeaderAccept, echo.MIMEApplicationJSON) 693 res, err := safehttp.ClientWithKeepAlive.Do(req) 694 if err != nil || res.StatusCode != http.StatusOK { 695 return "", claims, false 696 } 697 defer res.Body.Close() 698 body := &auth.AccessToken{} 699 if err = json.NewDecoder(res.Body).Decode(&body); err != nil || body.AccessToken == "" { 700 return "", claims, false 701 } 702 other := permission.Claims{} 703 _, _, err = new(jwt.Parser).ParseUnverified(body.AccessToken, &other) 704 if err != nil { 705 return "", claims, false 706 } 707 708 // Create a new refresh token 709 refresh, err := c.CreateJWT(i, consts.RefreshTokenAudience, claims.Scope) 710 return refresh, claims, err == nil 711 }