restapi_auth_oauth2.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. package otoauth2
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/tls"
  6. "crypto/x509"
  7. "encoding/base64"
  8. "encoding/json"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "os"
  13. "time"
  14. authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  15. config "github.com/OliveTin/OliveTin/internal/config"
  16. log "github.com/sirupsen/logrus"
  17. "golang.org/x/oauth2"
  18. )
  19. type OAuth2Handler struct {
  20. cfg *config.Config
  21. registeredStates map[string]*oauth2State
  22. registeredProviders map[string]*oauth2.Config
  23. }
  24. func NewOAuth2Handler(cfg *config.Config) *OAuth2Handler {
  25. h := &OAuth2Handler{
  26. cfg: cfg,
  27. }
  28. h.registeredStates = make(map[string]*oauth2State)
  29. h.registeredProviders = make(map[string]*oauth2.Config)
  30. for providerName, providerConfig := range cfg.AuthOAuth2Providers {
  31. completeProviderConfig(providerName, providerConfig)
  32. newConfig := &oauth2.Config{
  33. ClientID: providerConfig.ClientID,
  34. ClientSecret: providerConfig.ClientSecret,
  35. Scopes: providerConfig.Scopes,
  36. Endpoint: oauth2.Endpoint{
  37. AuthURL: providerConfig.AuthUrl,
  38. TokenURL: providerConfig.TokenUrl,
  39. },
  40. RedirectURL: cfg.AuthOAuth2RedirectURL,
  41. }
  42. h.registeredProviders[providerName] = newConfig
  43. log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
  44. }
  45. return h
  46. }
  47. type oauth2State struct {
  48. providerConfig *oauth2.Config
  49. providerName string
  50. Username string
  51. Usergroup string
  52. }
  53. func assignIfEmpty(target *string, value string) {
  54. if *target == "" {
  55. *target = value
  56. }
  57. }
  58. func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
  59. dbConfig, ok := oauth2ProviderDatabase[providerName]
  60. if ok {
  61. assignIfEmpty(&providerConfig.Name, dbConfig.Name)
  62. assignIfEmpty(&providerConfig.Title, dbConfig.Title)
  63. assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
  64. assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
  65. assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
  66. assignIfEmpty(&providerConfig.Icon, dbConfig.Icon)
  67. assignIfEmpty(&providerConfig.UsernameField, dbConfig.UsernameField)
  68. if providerConfig.Scopes == nil {
  69. providerConfig.Scopes = dbConfig.Scopes
  70. }
  71. } else {
  72. log.Warnf("Provider not found in database: %v", providerName)
  73. }
  74. }
  75. func (h *OAuth2Handler) getOAuth2Config(providerName string) (*oauth2.Config, error) {
  76. config, ok := h.registeredProviders[providerName]
  77. if !ok {
  78. return nil, fmt.Errorf("provider not found in config: %v", providerName)
  79. }
  80. return config, nil
  81. }
  82. func randString(nByte int) (string, error) {
  83. b := make([]byte, nByte)
  84. if _, err := io.ReadFull(rand.Reader, b); err != nil {
  85. return "", err
  86. }
  87. return base64.URLEncoding.EncodeToString(b), nil
  88. }
  89. func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
  90. cookie := &http.Cookie{
  91. Name: name,
  92. Value: value,
  93. MaxAge: 900, // 15 minutes
  94. Secure: r.TLS != nil,
  95. HttpOnly: true,
  96. Path: "/",
  97. }
  98. http.SetCookie(w, cookie)
  99. }
  100. func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
  101. state, err := randString(16)
  102. if err != nil {
  103. http.Error(w, err.Error(), http.StatusInternalServerError)
  104. return
  105. }
  106. providerName := r.URL.Query().Get("provider")
  107. provider, err := h.getOAuth2Config(providerName)
  108. if err != nil {
  109. log.Errorf("Failed to get provider config: %v %v", providerName, err)
  110. http.Error(w, err.Error(), http.StatusBadRequest)
  111. return
  112. }
  113. h.registeredStates[state] = &oauth2State{
  114. providerConfig: provider,
  115. providerName: providerName,
  116. Username: "",
  117. }
  118. h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
  119. log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
  120. http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
  121. }
  122. func (h *OAuth2Handler) validateStateMatch(queryState, cookieState string) bool {
  123. return queryState == cookieState
  124. }
  125. func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
  126. cookie, err := r.Cookie("olivetin-sid-oauth")
  127. if err != nil {
  128. log.Errorf("Failed to get state cookie: %v", err)
  129. http.Error(w, "State not found", http.StatusBadRequest)
  130. return nil, "", false
  131. }
  132. state := cookie.Value
  133. if !h.validateStateMatch(r.URL.Query().Get("state"), state) {
  134. log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state)
  135. http.Error(w, "State mismatch", http.StatusBadRequest)
  136. return nil, state, false
  137. }
  138. registeredState, ok := h.registeredStates[state]
  139. if !ok {
  140. log.Errorf("State not found in server: %v", state)
  141. http.Error(w, "State not found in server", http.StatusBadRequest)
  142. return nil, state, false
  143. }
  144. return registeredState, state, true
  145. }
  146. type HttpClientSettings struct {
  147. Transport *http.Transport
  148. Timeout time.Duration
  149. }
  150. func getOAuth2HttpClient(providerConfig *config.OAuth2Provider) *HttpClientSettings {
  151. config := &HttpClientSettings{
  152. Transport: &http.Transport{
  153. TLSClientConfig: &tls.Config{InsecureSkipVerify: providerConfig.InsecureSkipVerify},
  154. },
  155. Timeout: time.Duration(min(3, providerConfig.CallbackTimeout)) * time.Second,
  156. }
  157. if providerConfig.CertBundlePath != "" {
  158. config.Transport.TLSClientConfig.RootCAs = getOAuthCertBundle(providerConfig)
  159. }
  160. return config
  161. }
  162. func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
  163. caCert, err := os.ReadFile(providerConfig.CertBundlePath)
  164. if err != nil {
  165. log.Errorf("OAuth2 Cert Bundle - failed to read file: %v", err)
  166. return nil
  167. }
  168. caCertPool := x509.NewCertPool()
  169. if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
  170. log.Errorf("OAuth2 Cert Bundle - failed to append certificates from PEM")
  171. }
  172. return caCertPool
  173. }
  174. func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
  175. log.Infof("OAuth2 Callback received")
  176. registeredState, state, ok := h.checkOAuthCallbackCookie(w, r)
  177. if !ok {
  178. return
  179. }
  180. code := r.FormValue("code")
  181. log.WithFields(log.Fields{
  182. "state": state,
  183. "token-code": code,
  184. }).Debug("OAuth2 Token Code")
  185. providerConfig := h.cfg.AuthOAuth2Providers[registeredState.providerName]
  186. clientSettings := getOAuth2HttpClient(providerConfig)
  187. exchangeClient := &http.Client{
  188. Transport: clientSettings.Transport,
  189. Timeout: clientSettings.Timeout,
  190. }
  191. ctx := context.Background()
  192. ctx = context.WithValue(ctx, oauth2.HTTPClient, exchangeClient)
  193. tok, err := registeredState.providerConfig.Exchange(ctx, code)
  194. if err != nil {
  195. log.Errorf("Failed to exchange code: %v", err)
  196. http.Error(w, "Failed to exchange code", http.StatusBadRequest)
  197. return
  198. }
  199. userInfoClient := &http.Client{
  200. Transport: &oauth2.Transport{
  201. Source: registeredState.providerConfig.TokenSource(ctx, tok),
  202. Base: clientSettings.Transport,
  203. },
  204. Timeout: clientSettings.Timeout,
  205. }
  206. userinfo := getUserInfo(h.cfg, userInfoClient, h.cfg.AuthOAuth2Providers[registeredState.providerName])
  207. h.registeredStates[state].Username = userinfo.Username
  208. h.registeredStates[state].Usergroup = userinfo.Usergroup
  209. http.Redirect(w, r, "/", http.StatusFound)
  210. }
  211. type UserInfo struct {
  212. Username string
  213. Usergroup string
  214. }
  215. //gocyclo:ignore
  216. func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth2Provider) *UserInfo {
  217. ret := &UserInfo{}
  218. res, err := client.Get(provider.WhoamiUrl)
  219. if err != nil {
  220. log.Errorf("Failed to get user data: %v", err)
  221. return ret
  222. }
  223. if res.StatusCode != http.StatusOK {
  224. log.Errorf("Failed to get user data: %v", res.StatusCode)
  225. return ret
  226. }
  227. defer res.Body.Close()
  228. contents, err := io.ReadAll(res.Body)
  229. if err != nil {
  230. log.Errorf("Failed to read user data: %v", err)
  231. return ret
  232. }
  233. var userData map[string]any
  234. if cfg.InsecureAllowDumpOAuth2UserData {
  235. log.Debugf("OAuth2 User Data: %v+", string(contents))
  236. }
  237. err = json.Unmarshal([]byte(contents), &userData)
  238. if err != nil {
  239. log.Errorf("Failed to unmarshal user data: %v", err)
  240. return ret
  241. }
  242. ret.Username = getDataField(userData, provider.UsernameField)
  243. ret.Usergroup = getDataField(userData, provider.UserGroupField)
  244. return ret
  245. }
  246. func getDataField(data map[string]any, field string) string {
  247. if field == "" {
  248. return ""
  249. }
  250. val, ok := data[field]
  251. if !ok {
  252. log.Errorf("Failed to get field from user data: %v / %v", data, field)
  253. return ""
  254. }
  255. stringVal, ok := val.(string)
  256. if !ok {
  257. log.Errorf("Field %v is not a string: %v", field, val)
  258. return ""
  259. }
  260. return stringVal
  261. }
  262. func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
  263. cookie, err := context.Request.Cookie("olivetin-sid-oauth")
  264. user := &authTypes.AuthenticatedUser{}
  265. if err != nil {
  266. return nil
  267. }
  268. if cookie.Value == "" {
  269. return nil
  270. }
  271. serverState, found := h.registeredStates[cookie.Value]
  272. if !found {
  273. log.WithFields(log.Fields{
  274. "sid": cookie.Value,
  275. "provider": "oauth2",
  276. }).Warnf("Stale session")
  277. return nil
  278. }
  279. user.Username = serverState.Username
  280. user.UsergroupLine = serverState.Usergroup
  281. user.Provider = "oauth2"
  282. user.SID = cookie.Value
  283. return user
  284. }