You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

oauth2.go 3.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. // Copyright 2022-2023 Simon Ser <contact@emersion.fr>
  2. // Derived from https://git.sr.ht/~emersion/soju/tree/36d6cb19a4f90d217d55afb0b15318321baaad09/item/auth/oauth2.go
  3. // Originally released under the AGPLv3, relicensed to the Ergo project under the MIT license
  4. // Modifications copyright 2024 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  5. // Released under the MIT license
  6. package oauth2
  7. import (
  8. "context"
  9. "encoding/json"
  10. "fmt"
  11. "net/http"
  12. "net/url"
  13. "strings"
  14. "time"
  15. )
  16. var (
  17. ErrAuthDisabled = fmt.Errorf("OAuth 2.0 authentication is disabled")
  18. // all cases where the infrastructure is working correctly, but we determined
  19. // that the user supplied an invalid token
  20. ErrInvalidToken = fmt.Errorf("OAuth 2.0 bearer token invalid")
  21. )
  22. type OAuth2BearerConfig struct {
  23. Enabled bool `yaml:"enabled"`
  24. Autocreate bool `yaml:"autocreate"`
  25. AuthScript bool `yaml:"auth-script"`
  26. IntrospectionURL string `yaml:"introspection-url"`
  27. IntrospectionTimeout time.Duration `yaml:"introspection-timeout"`
  28. // omit for `none`, required for `client_secret_basic`
  29. ClientID string `yaml:"client-id"`
  30. ClientSecret string `yaml:"client-secret"`
  31. }
  32. func (o *OAuth2BearerConfig) Postprocess() error {
  33. if !o.Enabled {
  34. return nil
  35. }
  36. if o.IntrospectionTimeout == 0 {
  37. return fmt.Errorf("a nonzero oauthbearer introspection timeout is required (try 10s)")
  38. }
  39. if _, err := url.Parse(o.IntrospectionURL); err != nil {
  40. return fmt.Errorf("invalid introspection-url: %w", err)
  41. }
  42. return nil
  43. }
  44. func (o *OAuth2BearerConfig) Introspect(ctx context.Context, token string) (username string, err error) {
  45. if !o.Enabled {
  46. return "", ErrAuthDisabled
  47. }
  48. ctx, cancel := context.WithTimeout(ctx, o.IntrospectionTimeout)
  49. defer cancel()
  50. reqValues := make(url.Values)
  51. reqValues.Set("token", token)
  52. reqBody := strings.NewReader(reqValues.Encode())
  53. req, err := http.NewRequestWithContext(ctx, http.MethodPost, o.IntrospectionURL, reqBody)
  54. if err != nil {
  55. return "", fmt.Errorf("failed to create OAuth 2.0 introspection request: %w", err)
  56. }
  57. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  58. req.Header.Set("Accept", "application/json")
  59. if o.ClientID != "" {
  60. req.SetBasicAuth(url.QueryEscape(o.ClientID), url.QueryEscape(o.ClientSecret))
  61. }
  62. resp, err := http.DefaultClient.Do(req)
  63. if err != nil {
  64. return "", fmt.Errorf("failed to send OAuth 2.0 introspection request: %v", err)
  65. }
  66. defer resp.Body.Close()
  67. if resp.StatusCode != http.StatusOK {
  68. return "", fmt.Errorf("OAuth 2.0 introspection error: %v", resp.Status)
  69. }
  70. var data oauth2Introspection
  71. if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
  72. return "", fmt.Errorf("failed to decode OAuth 2.0 introspection response: %v", err)
  73. }
  74. if !data.Active {
  75. return "", ErrInvalidToken
  76. }
  77. if data.Username == "" {
  78. // We really need the username here, otherwise an OAuth 2.0 user can
  79. // impersonate any other user.
  80. return "", fmt.Errorf("missing username in OAuth 2.0 introspection response")
  81. }
  82. return data.Username, nil
  83. }
  84. type oauth2Introspection struct {
  85. Active bool `json:"active"`
  86. Username string `json:"username"`
  87. }