Rename root, api and sdk module paths (imports, go.mod, go_package, ldflags, build scripts, buf configs) and regenerate protobuf/gateway code with the project's pinned toolchain. Wire identifiers unchanged. api gets a local replace for sdk; root/api/sdk require v0.1.0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
164 lines
5.8 KiB
Go
164 lines
5.8 KiB
Go
// Copyright IBM Corp. 2020, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"time"
|
|
|
|
"repo.anhonesthost.net/CyberCoveLLC/threshold/internal/auth/oidc/request"
|
|
"repo.anhonesthost.net/CyberCoveLLC/threshold/internal/authtoken"
|
|
"repo.anhonesthost.net/CyberCoveLLC/threshold/internal/db/timestamp"
|
|
"repo.anhonesthost.net/CyberCoveLLC/threshold/internal/errors"
|
|
"github.com/hashicorp/cap/oidc"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
// StartAuth accepts a request to start an OIDC authentication/authorization
|
|
// attempt. It returns two URLs and a tokenId. authUrl is an OIDC authorization
|
|
// request URL. The authUrl includes a "state" parameter which is encrypted and
|
|
// has a payload which includes (among other things) the final redirect
|
|
// (calculated from the clientInfo), a token_request_id, and nonce. The tokenUrl
|
|
// is the URL theclient can use to retrieve the results of the user's OIDC
|
|
// authentication attempt. The tokenId is an encrypted payload for the POST
|
|
// request to the tokenUrl.
|
|
//
|
|
// If the auth method is in an InactiveState, then an error is returned.
|
|
//
|
|
// Options supported:
|
|
//
|
|
// WithRoundTripPayload(string) provides an option for a client roundtrip
|
|
// payload. This payload will be added to the final redirect as a query
|
|
// parameter.
|
|
func StartAuth(ctx context.Context, oidcRepoFn OidcRepoFactory, authMethodId string, opt ...Option) (authUrl *url.URL, tokenId string, e error) {
|
|
const op = "oidc.StartAuth"
|
|
if authMethodId == "" {
|
|
return nil, "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
|
}
|
|
if oidcRepoFn == nil {
|
|
return nil, "", errors.New(ctx, errors.InvalidParameter, op, "missing oidc repo function")
|
|
}
|
|
r, err := oidcRepoFn()
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
am, err := r.lookupAuthMethod(ctx, authMethodId)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
if am == nil {
|
|
return nil, "", errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("auth method %s not found", authMethodId))
|
|
}
|
|
if am.OperationalState == string(InactiveState) {
|
|
return nil, "", errors.New(ctx, errors.AuthMethodInactive, op, "not allowed to start authentication attempt")
|
|
}
|
|
if len(am.Prompts) > 0 {
|
|
prompts := strutil.RemoveDuplicatesStable(am.Prompts, false)
|
|
|
|
if strutil.StrListContains(prompts, string(oidc.None)) && len(prompts) > 1 {
|
|
return nil, "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf(`prompts (%s) includes "none" with other values`, am.Prompts))
|
|
}
|
|
}
|
|
|
|
// get the provider from the cache (if possible)
|
|
provider, err := providerCache().get(ctx, am)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
callbackRedirect := fmt.Sprintf(CallbackEndpoint, am.GetApiUrl())
|
|
|
|
opts := getOpts(opt...)
|
|
finalRedirect := fmt.Sprintf(FinalRedirectEndpoint, am.GetApiUrl())
|
|
if opts.withRoundtripPayload != "" {
|
|
u := make(url.Values)
|
|
u.Add("roundtrip_payload", opts.withRoundtripPayload)
|
|
finalRedirect = fmt.Sprintf("%s?%s", finalRedirect, u.Encode())
|
|
}
|
|
now := time.Now()
|
|
createTime := timestamppb.New(now.Truncate(time.Second))
|
|
exp := timestamppb.New(now.Add(AttemptExpiration).Truncate(time.Second))
|
|
tokenRequestId, err := authtoken.NewAuthTokenId(ctx)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
nonce, err := oidc.NewID()
|
|
if err != nil {
|
|
return nil, "", errors.New(ctx, errors.Unknown, op, "unable to generate nonce", errors.WithWrap(err))
|
|
}
|
|
hash, err := provider.ConfigHash()
|
|
if err != nil {
|
|
return nil, "", errors.New(ctx, errors.Unknown, op, "unable to get provider config hash", errors.WithWrap(err))
|
|
}
|
|
st := &request.State{
|
|
TokenRequestId: tokenRequestId,
|
|
CreateTime: ×tamp.Timestamp{Timestamp: createTime},
|
|
ExpirationTime: ×tamp.Timestamp{Timestamp: exp},
|
|
FinalRedirectUrl: finalRedirect,
|
|
Nonce: nonce,
|
|
ProviderConfigHash: hash,
|
|
}
|
|
|
|
requestWrapper, err := requestWrappingWrapper(ctx, r.kms, am.ScopeId, authMethodId)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
encodedEncryptedSt, err := encryptMessage(ctx, requestWrapper, am, st)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
oidcOpts := []oidc.Option{
|
|
oidc.WithState(string(encodedEncryptedSt)),
|
|
oidc.WithNonce(nonce),
|
|
}
|
|
switch {
|
|
case am.MaxAge == -1:
|
|
oidcOpts = append(oidcOpts, oidc.WithMaxAge(0))
|
|
case am.MaxAge > 0:
|
|
oidcOpts = append(oidcOpts, oidc.WithMaxAge(uint(am.MaxAge)))
|
|
default:
|
|
}
|
|
|
|
if len(am.ClaimsScopes) > 0 {
|
|
oidcOpts = append(oidcOpts, oidc.WithScopes(am.ClaimsScopes...))
|
|
}
|
|
|
|
if len(am.Prompts) > 0 {
|
|
prompts := convertToOIDCPrompts(ctx, am.Prompts)
|
|
oidcOpts = append(oidcOpts, oidc.WithPrompts(prompts...))
|
|
}
|
|
|
|
// a bare min oidc.Request needed for the provider.AuthURL(...) call. We've intentionally not populated
|
|
// things like Audiences, because this oidc.Request isn't cached and not intended for use in future legs
|
|
// of the authen flow.
|
|
oidcReq, err := oidc.NewRequest(
|
|
AttemptExpiration,
|
|
callbackRedirect,
|
|
oidcOpts...)
|
|
if err != nil {
|
|
return nil, "", errors.New(ctx, errors.Unknown, op, "unable to create oidc request", errors.WithWrap(err))
|
|
}
|
|
|
|
u, err := provider.AuthURL(ctx, oidcReq)
|
|
if err != nil {
|
|
return nil, "", errors.New(ctx, errors.Unknown, op, "unable to get auth url from provider", errors.WithWrap(err))
|
|
}
|
|
authUrl, err = url.Parse(u)
|
|
if err != nil {
|
|
return nil, "", errors.New(ctx, errors.Unknown, op, "unable to parse auth url", errors.WithWrap(err))
|
|
}
|
|
|
|
t := &request.Token{
|
|
RequestId: tokenRequestId,
|
|
ExpirationTime: ×tamp.Timestamp{Timestamp: exp},
|
|
}
|
|
encodedEncryptedTk, err := encryptMessage(ctx, requestWrapper, am, t)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(ctx, err, op)
|
|
}
|
|
return authUrl, encodedEncryptedTk, nil
|
|
}
|