/* Copyright CAcert Inc. SPDX-License-Identifier: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package services import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "net/http" "net/url" "time" "github.com/lestrrat-go/jwx/jwk" "golang.org/x/oauth2" "code.cacert.org/cacert/oidc-demo-app/internal/models" ) // OidcParams defines the parameters for DiscoverOIDC type OidcParams struct { OidcServer string OidcClientID string OidcClientSecret string APIClient *http.Client } type OIDCInformation struct { KeySet jwk.Set OAuth2Config *oauth2.Config OIDCConfiguration *models.OpenIDConfiguration } // DiscoverOIDC gets OpenID Connect parameters from the discovery endpoint and the // JSON Web Key Set from the discovered jwksUri. // // The subset of values specified by models.OpenIDConfiguration is stored in // the given context and can be retrieved from the context by GetOidcConfig. // // OAuth2 specific values are stored in another context object and can be // retrieved by GetOAuth2Config. // // The JSON Web Key Set can be retrieved by GetJwkSet. func DiscoverOIDC(logger *slog.Logger, params *OidcParams) (*OIDCInformation, error) { discoveryURL, err := url.Parse(params.OidcServer) if err != nil { logger.Error( "could not parse parameter oidc.server as URL", "oidc.server", params.OidcServer, ) return nil, fmt.Errorf("could not parse parameter value: %w", err) } discoveryURL.Path = "/.well-known/openid-configuration" var ( body []byte req *http.Request ) req, err = http.NewRequest(http.MethodGet, discoveryURL.String(), bytes.NewBuffer(body)) if err != nil { return nil, fmt.Errorf("could not create OIDC discovery request: %w", err) } req.Header = map[string][]string{ "Accept": {"application/json"}, } resp, err := params.APIClient.Do(req) if err != nil { return nil, fmt.Errorf("call to OIDC discovery endpoint failed: %w", err) } defer func() { _ = resp.Body.Close() }() dec := json.NewDecoder(resp.Body) discoveryResponse := &models.OpenIDConfiguration{} err = dec.Decode(discoveryResponse) if err != nil { return nil, fmt.Errorf("could not decode OIDC discovery response: %w", err) } oauth2Config := &oauth2.Config{ ClientID: params.OidcClientID, ClientSecret: params.OidcClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: discoveryResponse.AuthorizationEndpoint, TokenURL: discoveryResponse.TokenEndpoint, }, Scopes: []string{"openid", "email", "profile"}, } const jwkFetchTimeout = 10 * time.Second ctx, cancel := context.WithTimeout(context.Background(), jwkFetchTimeout) defer cancel() keySet, err := jwk.Fetch(ctx, discoveryResponse.JwksURI, jwk.WithHTTPClient(params.APIClient)) if err != nil { return nil, fmt.Errorf("could not fetch JWKs: %w", err) } return &OIDCInformation{ KeySet: keySet, OAuth2Config: oauth2Config, OIDCConfiguration: discoveryResponse, }, nil }