Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabling OAuth for the origin and cache, adding support for Globus as another auth server #963

Merged
merged 16 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions broker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func ConnectToOrigin(ctx context.Context, brokerUrl, prefix, originName string)
}()

// Send a request to the broker for a connection reversal
req, err := http.NewRequestWithContext(ctx, "POST", brokerUrl, reqReader)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, brokerUrl, reqReader)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "pelican-cache/"+config.GetVersion())

Expand Down Expand Up @@ -397,7 +397,7 @@ func doCallback(ctx context.Context, brokerResp reversalRequest) (listener net.L
}
reqReader := bytes.NewReader(reqBytes)
var req *http.Request
req, err = http.NewRequestWithContext(ctx, "POST", brokerResp.CallbackUrl, reqReader)
req, err = http.NewRequestWithContext(ctx, http.MethodPost, brokerResp.CallbackUrl, reqReader)
if err != nil {
return
}
Expand Down Expand Up @@ -577,7 +577,7 @@ func LaunchRequestMonitor(ctx context.Context, egrp *errgroup.Group, resultChan
default:
// Send a request to the broker for a connection reversal
reqReader.Reset(req)
req, err := http.NewRequestWithContext(ctx, "POST", brokerEndpoint, reqReader)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, brokerEndpoint, reqReader)
if err != nil {
log.Errorln("Failure when creating new broker URL request:", err)
break
Expand Down
2 changes: 1 addition & 1 deletion cache/self_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func downloadTestFile(ctx context.Context, fileUrl string) error {
return errors.Wrap(err, "failed to create a token for cache self-test download")
}

req, err := http.NewRequestWithContext(ctx, "GET", fileUrl, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
if err != nil {
return errors.Wrap(err, "failed to create GET request for cache self-test download")
}
Expand Down
2 changes: 1 addition & 1 deletion client/get_best_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func GetBestCache(cacheListName string) ([]string, error) {
log.Debugln("Querying", GeoIpUrl.String())
}
client := &http.Client{Transport: defaultTransport}
req, err := http.NewRequest("GET", GeoIpUrl.String(), nil)
req, err := http.NewRequest(http.MethodGet, GeoIpUrl.String(), nil)
if err != nil {
log.Errorln("Failed to create HTTP request:", err)
skipResponse = true
Expand Down
10 changes: 5 additions & 5 deletions client/handle_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -1390,7 +1390,7 @@ func sortAttempts(ctx context.Context, path string, attempts []transferAttemptDe

go func(idx int, tUrl string) {
headClient := &http.Client{Transport: transport}
headRequest, _ := http.NewRequestWithContext(ctx, "HEAD", tUrl, nil)
headRequest, _ := http.NewRequestWithContext(ctx, http.MethodHead, tUrl, nil)
var headResponse *http.Response
headResponse, err := headClient.Do(headRequest)
if err != nil {
Expand Down Expand Up @@ -1704,7 +1704,7 @@ func downloadHTTP(ctx context.Context, te *TransferEngine, callback TransferCall
// Do a head request for content length if resp.Size is unknown
if totalSize <= 0 && !resp.IsComplete() {
headClient := &http.Client{Transport: transport}
headRequest, _ := http.NewRequest("HEAD", transferUrl.String(), nil)
headRequest, _ := http.NewRequest(http.MethodHead, transferUrl.String(), nil)
var headResponse *http.Response
headResponse, err = headClient.Do(headRequest)
if err != nil {
Expand Down Expand Up @@ -1996,9 +1996,9 @@ func uploadObject(transfer *transferFile) (transferResult TransferResults, err e
var request *http.Request
// For files that are 0 length, we need to send a PUT request with an nil body
if nonZeroSize {
request, err = http.NewRequestWithContext(putContext, "PUT", dest.String(), reader)
request, err = http.NewRequestWithContext(putContext, http.MethodPut, dest.String(), reader)
} else {
request, err = http.NewRequestWithContext(putContext, "PUT", dest.String(), http.NoBody)
request, err = http.NewRequestWithContext(putContext, http.MethodPut, dest.String(), http.NoBody)
}
if err != nil {
log.Errorln("Error creating request:", err)
Expand Down Expand Up @@ -2311,7 +2311,7 @@ func statHttp(ctx context.Context, dest *url.URL, namespace namespaces.Namespace
}

var req *http.Request
req, err = http.NewRequestWithContext(ctx, "HEAD", endpoint.String(), nil)
req, err = http.NewRequestWithContext(ctx, http.MethodHead, endpoint.String(), nil)
if err != nil {
log.Errorln("Failed to create HTTP request:", err)
resultsChan <- statResults{0, err}
Expand Down
14 changes: 11 additions & 3 deletions config/issuer_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ package config

import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"

"github.com/pkg/errors"
)

type OauthIssuer struct {
Expand All @@ -37,10 +38,17 @@ type OauthIssuer struct {
ScopesSupported []string `json:"scopes_supported"`
}

// Get OIDC issuer metadata from an OIDC issuer URL.
// The URL should not contain the path to /.well-known/openid-configuration
func GetIssuerMetadata(issuer_url string) (*OauthIssuer, error) {
haoming29 marked this conversation as resolved.
Show resolved Hide resolved
wellKnownUrl := strings.TrimSuffix(issuer_url, "/") + "/.well-known/openid-configuration"

resp, err := http.Get(wellKnownUrl)
client := http.Client{Transport: GetTransport()}
req, err := http.NewRequest(http.MethodGet, wellKnownUrl, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
Expand All @@ -52,7 +60,7 @@ func GetIssuerMetadata(issuer_url string) (*OauthIssuer, error) {
}

if resp.StatusCode != 200 {
return nil, errors.New("Failed to retrieve issuer metadata")
return nil, errors.Errorf("Failed to retrieve issuer metadata at %s with status code %d", wellKnownUrl, resp.StatusCode)
}

issuer := &OauthIssuer{}
Expand Down
58 changes: 39 additions & 19 deletions config/oidc_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package config

import (
"net/url"
"os"
"strings"
"sync"
Expand Down Expand Up @@ -47,53 +48,72 @@ func getMetadata() {
metadataError = errors.New("OIDC.Issuer is not set; unable to do metadata discovery")
return
}
if _, err := url.Parse(issuerUrl); err != nil {
metadataError = errors.Wrap(err, "OIDC.Issuer is not a valid URL; unable to do metadata discovery")
return
}
log.Debugln("Getting OIDC issuer metadata via URL", issuerUrl)
metadata, err := GetIssuerMetadata(issuerUrl)
if err != nil {
log.Warningf("Failed to get OIDC issuer metadata with error %v. Fall back to CILogon endpoints for OIDC authentication if individual OIDC endpoints are not set.", err)
metadataError = err
return
}
oidcMetadata = metadata

if param.OIDC_DeviceAuthEndpoint.GetString() == "" {
viper.Set("OIDC.DeviceAuthEndpoint", metadata.DeviceAuthURL)
}
if param.OIDC_TokenEndpoint.GetString() == "" {
viper.Set("OIDC.TokenEndpoint", metadata.TokenURL)
}
if param.OIDC_UserInfoEndpoint.GetString() == "" {
viper.Set("OIDC.UserInfoEndpoint", metadata.UserInfoURL)
}
if param.OIDC_AuthorizationEndpoint.GetString() == "" {
viper.Set("OIDC.AuthorizationEndpoint", metadata.AuthURL)
}
// We don't check if the endpoint(s) are set. Just overwrite to ensure
// our default values are not being used if the issuer is not CILogon
viper.Set("OIDC.DeviceAuthEndpoint", metadata.DeviceAuthURL)
viper.Set("OIDC.TokenEndpoint", metadata.TokenURL)
viper.Set("OIDC.UserInfoEndpoint", metadata.UserInfoURL)
viper.Set("OIDC.AuthorizationEndpoint", metadata.AuthURL)
}

func getMetadataValue(metadataFunc func() string) (result string, err error) {
func getMetadataValue(stringParam param.StringParam) (result string, err error) {
onceMetadata.Do(getMetadata)
result = metadataFunc()
result = stringParam.GetString()
// Assume if the OIDC value is set then that was from the config file
// so we skip any errors
if result == "" {
err = metadataError
// A hacky way to allow Globus as an auth server
if param.OIDC_Issuer.IsSet() {
issuerUrl, _ := url.Parse(param.OIDC_Issuer.GetString())
if issuerUrl != nil && issuerUrl.Hostname() == "auth.globus.org" && stringParam.GetName() == param.OIDC_DeviceAuthEndpoint.GetName() {
log.Warning("You are using Globus as the auth privider. Although it does not support OAuth device flow used by Pelican registry, you may use it for other Pelican servers. OIDC.DeviceAuthEndpoint is set to https://auth.globus.org/")
result = "https://auth.globus.org/"
return
}
}

if metadataError == nil {
err = errors.Errorf("Required OIDC endpoint %s is not set and OIDC discovery at %s doesn't have the endpoint in the metadata. Your authentication server may not support OAuth2 authorization flow that is required by Pelican.",
stringParam.GetName(),
param.OIDC_Issuer.GetString(),
)
} else {
err = errors.Wrapf(metadataError,
"Required OIDC endpoint %s is not set and OIDC discovery failed to request metadata from OIDC.Issuer",
stringParam.GetName(),
)
}
}
return
}

func GetOIDCDeviceAuthEndpoint() (result string, err error) {
return getMetadataValue(param.OIDC_DeviceAuthEndpoint.GetString)
return getMetadataValue(param.OIDC_DeviceAuthEndpoint)
}

func GetOIDCTokenEndpoint() (result string, err error) {
return getMetadataValue(param.OIDC_TokenEndpoint.GetString)
return getMetadataValue(param.OIDC_TokenEndpoint)
}

func GetOIDCUserInfoEndpoint() (result string, err error) {
return getMetadataValue(param.OIDC_UserInfoEndpoint.GetString)
return getMetadataValue(param.OIDC_UserInfoEndpoint)
}

func GetOIDCAuthorizationEndpoint() (result string, err error) {
return getMetadataValue(param.OIDC_AuthorizationEndpoint.GetString)
return getMetadataValue(param.OIDC_AuthorizationEndpoint)
}

func GetOIDCSupportedScopes() (results []string, err error) {
Expand Down
2 changes: 1 addition & 1 deletion director/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func reportStatusToServer(ctx context.Context, serverWebUrl string, status strin
reqBody := bytes.NewBuffer(jsonData)

log.Debugf("Director is sending %s server test result to %s", string(serverType), reportUrl.String())
req, err := http.NewRequestWithContext(ctx, "POST", reportUrl.String(), reqBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reportUrl.String(), reqBody)
if err != nil {
return errors.Wrap(err, "failed to create POST request for reporting director test")
}
Expand Down
2 changes: 1 addition & 1 deletion director/origin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func checkNamespaceStatus(prefix string, registryWebUrlStr string) (bool, error)
return false, err
}
client := http.Client{Transport: config.GetTransport()}
req, err := http.NewRequest("POST", reqUrl.String(), bytes.NewBuffer(reqByte))
req, err := http.NewRequest(http.MethodPost, reqUrl.String(), bytes.NewBuffer(reqByte))
req.Header.Add("Content-Type", "application/json")
if err != nil {
return false, err
Expand Down
2 changes: 1 addition & 1 deletion director/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (stat *ObjectStat) sendHeadReqToOrigin(objectName string, dataUrl url.URL,

client := http.Client{Transport: config.GetTransport(), Timeout: timeout}
reqUrl := dataUrl.JoinPath(objectName)
req, err := http.NewRequestWithContext(ctx, "HEAD", reqUrl.String(), nil)
req, err := http.NewRequestWithContext(ctx, http.MethodHead, reqUrl.String(), nil)
if err != nil {
return nil, errors.Wrap(err, "Error creating request")
}
Expand Down
15 changes: 11 additions & 4 deletions docs/pages/serving_a_federation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ The Pelican *registry* manages namespace registration and public key distributio

The Pelican registry follows [OIDC](https://openid.net/developers/how-connect-works/) for user authentication, and [CILogon](https://www.cilogon.org/) is our OpenID Provider by default, which enables single sign-on for users with an account associated with an institution that joins CILogon. (Check out [this page](https://cilogon.org) for institutions in CILogon)

For your Pelican registry to work, you need to obtain a client id and associated client secret from CILogon. [This page](https://www.cilogon.org/oidc#h.p_ID_38) details how you can request a client credential. You will need to register your client at https://cilogon.org/oauth2/register and wait for approval before proceeding.
For Pelican registry to work, you need to obtain a client id and associated client secret from CILogon. [This page](https://www.cilogon.org/oidc#h.p_ID_38) details how you can request a client credential. You will need to register your client at https://cilogon.org/oauth2/register and wait for approval before proceeding. Below is a guidance on how to fill in the registration form for CILogon.

* **`Client Name`**: a human-readable name of your service. For example: Pelican Data Federation
* **`Callback URLs`**: fill in `https://<hostname>:<server_port>/api/v1.0/auth/oauth/callback` where `<hostname>:<server_port>` is your server's public endpoint. For example, `https://example-origin.org:8444/api/v1.0/auth/oauth/callback`
* **`Client Type`**: select `Confedential`
* **`Scopes`**: select `email`, `openid`, `org.cilogon.userinfo`, and `profile`
* **`Refresh Tokens`**: select `No`

Once approved, you will get your `client_id` and `client_secret` from CILogon, and you will need to pass them as configuration parameters and configuration files to Pelican.
Once approved, you will get your `client_id` and `client_secret` from CILogon. Pass them as configuration parameters and configuration files to Pelican.

* Set the `OIDC.ClientID` config parameter to your `<client_id>` value.
* Create a file named `/etc/pelican/oidc-client-secret`
Expand All @@ -38,7 +43,7 @@ Once approved, you will get your `client_id` and `client_secret` from CILogon, a
touch /etc/pelican/oidc-client-secret
```

* Copy and paste your `client_secret` into the file. Please don't share your `client_secret`.
* Copy and paste your `client_secret` into the file you just created. Please don't share your `client_secret`.

If you prefer to store your `client_secret` file in a path different from the default file path, change `OIDC.ClientSecretFile` to your desired file location.

Expand Down Expand Up @@ -89,7 +94,9 @@ The homepage of the registry web UI is also publicly accessible, meaning users c

There are a couple of configuration parameters you could use to customize the behavior of your registry. Here we highlight the ones that are most frequently set for an admin. You may refer to the full set of registry parameters in the [Parameters page](./parameters.mdx#Registry-DbLocation).

#### `Registry.AdminUsers`
#### `Registry.AdminUsers` [Deprecated]

> `Registry.AdminUsers` is deprecated in Pelican `v7.7.0`. Use `OIDC.AdminUsers` instead.

By default, Pelican registry only has one user with admin privilege, which is whoever starts the registry service and initializes the web UI with the admin password.

Expand Down
Loading
Loading