feat: map info from OIDC claims to headers (#122)

* refactor: return all values from body in the providers

* refactor: only accept claims following the OIDC spec

* feat: map info from OIDC claims to headers

* feat: add support for required oauth groups

* fix: bot suggestions

* feat: get claims from github and google

* fix: close body correctly
This commit is contained in:
Stavros
2025-04-30 19:57:49 +03:00
committed by GitHub
parent f824b84787
commit a9e8bf89a9
24 changed files with 528 additions and 210 deletions

View File

@@ -4,24 +4,25 @@ import (
"encoding/json"
"io"
"net/http"
"tinyauth/internal/constants"
"github.com/rs/zerolog/log"
)
// We are assuming that the generic provider will return a JSON object with an email field
type GenericUserInfoResponse struct {
Email string `json:"email"`
}
func GetGenericUser(client *http.Client, url string) (constants.Claims, error) {
// Create user struct
var user constants.Claims
func GetGenericEmail(client *http.Client, url string) (string, error) {
// Using the oauth client get the user info url
res, err := client.Get(url)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
defer res.Body.Close()
log.Debug().Msg("Got response from generic provider")
// Read the body of the response
@@ -29,24 +30,21 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Read body from generic provider")
// Parse the body into a user struct
var user GenericUserInfoResponse
// Unmarshal the body into the user struct
err = json.Unmarshal(body, &user)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Parsed user from generic provider")
// Return the email
return user.Email, nil
// Return the user
return user, nil
}

View File

@@ -5,51 +5,96 @@ import (
"errors"
"io"
"net/http"
"tinyauth/internal/constants"
"github.com/rs/zerolog/log"
)
// Github has a different response than the generic provider
type GithubUserInfoResponse []struct {
// Response for the github email endpoint
type GithubEmailResponse []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
}
// The scopes required for the github provider
func GithubScopes() []string {
return []string{"user:email"}
// Response for the github user endpoint
type GithubUserInfoResponse struct {
Login string `json:"login"`
Name string `json:"name"`
}
func GetGithubEmail(client *http.Client) (string, error) {
// Get the user emails from github using the oauth http client
res, err := client.Get("https://api.github.com/user/emails")
// The scopes required for the github provider
func GithubScopes() []string {
return []string{"user:email", "read:user"}
}
func GetGithubUser(client *http.Client) (constants.Claims, error) {
// Create user struct
var user constants.Claims
// Get the user info from github using the oauth http client
res, err := client.Get("https://api.github.com/user")
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Got response from github")
defer res.Body.Close()
log.Debug().Msg("Got user response from github")
// Read the body of the response
body, err := io.ReadAll(res.Body)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Read body from github")
log.Debug().Msg("Read user body from github")
// Parse the body into a user struct
var emails GithubUserInfoResponse
var userInfo GithubUserInfoResponse
// Unmarshal the body into the user struct
err = json.Unmarshal(body, &userInfo)
// Check if there was an error
if err != nil {
return user, err
}
// Get the user emails from github using the oauth http client
res, err = client.Get("https://api.github.com/user/emails")
// Check if there was an error
if err != nil {
return user, err
}
defer res.Body.Close()
log.Debug().Msg("Got email response from github")
// Read the body of the response
body, err = io.ReadAll(res.Body)
// Check if there was an error
if err != nil {
return user, err
}
log.Debug().Msg("Read email body from github")
// Parse the body into a user struct
var emails GithubEmailResponse
// Unmarshal the body into the user struct
err = json.Unmarshal(body, &emails)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Parsed emails from github")
@@ -57,10 +102,26 @@ func GetGithubEmail(client *http.Client) (string, error) {
// Find and return the primary email
for _, email := range emails {
if email.Primary {
return email.Email, nil
// Set the email then exit
user.Email = email.Email
break
}
}
// User does not have a primary email?
return "", errors.New("no primary email found")
// If no primary email was found, use the first available email
if len(emails) == 0 {
return user, errors.New("no emails found")
}
// Set the email if it is not set picking the first one
if user.Email == "" {
user.Email = emails[0].Email
}
// Set the username and name
user.PreferredUsername = userInfo.Login
user.Name = userInfo.Name
// Return
return user, nil
}

View File

@@ -4,29 +4,37 @@ import (
"encoding/json"
"io"
"net/http"
"strings"
"tinyauth/internal/constants"
"github.com/rs/zerolog/log"
)
// Google works the same as the generic provider
// Response for the google user endpoint
type GoogleUserInfoResponse struct {
Email string `json:"email"`
Name string `json:"name"`
}
// The scopes required for the google provider
func GoogleScopes() []string {
return []string{"https://www.googleapis.com/auth/userinfo.email"}
return []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}
}
func GetGoogleEmail(client *http.Client) (string, error) {
func GetGoogleUser(client *http.Client) (constants.Claims, error) {
// Create user struct
var user constants.Claims
// Get the user info from google using the oauth http client
res, err := client.Get("https://www.googleapis.com/userinfo/v2/me")
// Check if there was an error
if err != nil {
return "", err
return user, err
}
defer res.Body.Close()
log.Debug().Msg("Got response from google")
// Read the body of the response
@@ -34,24 +42,29 @@ func GetGoogleEmail(client *http.Client) (string, error) {
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Read body from google")
// Parse the body into a user struct
var user GoogleUserInfoResponse
// Create a new user info struct
var userInfo GoogleUserInfoResponse
// Unmarshal the body into the user struct
err = json.Unmarshal(body, &user)
err = json.Unmarshal(body, &userInfo)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Parsed user from google")
// Return the email
return user.Email, nil
// Map the user info to the user struct
user.PreferredUsername = strings.Split(userInfo.Email, "@")[0]
user.Name = userInfo.Name
user.Email = userInfo.Email
// Return the user
return user, nil
}

View File

@@ -2,6 +2,7 @@ package providers
import (
"fmt"
"tinyauth/internal/constants"
"tinyauth/internal/oauth"
"tinyauth/internal/types"
@@ -93,14 +94,17 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
}
}
func (providers *Providers) GetUser(provider string) (string, error) {
// Get the email from the provider
func (providers *Providers) GetUser(provider string) (constants.Claims, error) {
// Create user struct
var user constants.Claims
// Get the user from the provider
switch provider {
case "github":
// If the github provider is not configured, return an error
if providers.Github == nil {
log.Debug().Msg("Github provider not configured")
return "", nil
return user, nil
}
// Get the client from the github provider
@@ -108,23 +112,23 @@ func (providers *Providers) GetUser(provider string) (string, error) {
log.Debug().Msg("Got client from github")
// Get the email from the github provider
email, err := GetGithubEmail(client)
// Get the user from the github provider
user, err := GetGithubUser(client)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Got email from github")
log.Debug().Msg("Got user from github")
// Return the email
return email, nil
// Return the user
return user, nil
case "google":
// If the google provider is not configured, return an error
if providers.Google == nil {
log.Debug().Msg("Google provider not configured")
return "", nil
return user, nil
}
// Get the client from the google provider
@@ -132,23 +136,23 @@ func (providers *Providers) GetUser(provider string) (string, error) {
log.Debug().Msg("Got client from google")
// Get the email from the google provider
email, err := GetGoogleEmail(client)
// Get the user from the google provider
user, err := GetGoogleUser(client)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Got email from google")
log.Debug().Msg("Got user from google")
// Return the email
return email, nil
// Return the user
return user, nil
case "generic":
// If the generic provider is not configured, return an error
if providers.Generic == nil {
log.Debug().Msg("Generic provider not configured")
return "", nil
return user, nil
}
// Get the client from the generic provider
@@ -156,20 +160,20 @@ func (providers *Providers) GetUser(provider string) (string, error) {
log.Debug().Msg("Got client from generic")
// Get the email from the generic provider
email, err := GetGenericEmail(client, providers.Config.GenericUserURL)
// Get the user from the generic provider
user, err := GetGenericUser(client, providers.Config.GenericUserURL)
// Check if there was an error
if err != nil {
return "", err
return user, err
}
log.Debug().Msg("Got email from generic")
log.Debug().Msg("Got user from generic")
// Return the email
return email, nil
return user, nil
default:
return "", nil
return user, nil
}
}