Start implementation of individual claim handling
This commit is contained in:
parent
e9c34a2337
commit
744440ee54
6 changed files with 243 additions and 12 deletions
|
@ -1,22 +1,26 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/jwk"
|
"github.com/lestrrat-go/jwx/jwk"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/jwt"
|
||||||
"github.com/lestrrat-go/jwx/jwt/openid"
|
"github.com/lestrrat-go/jwx/jwt/openid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"git.cacert.org/oidc_login/app/services"
|
"git.cacert.org/oidc_login/app/services"
|
||||||
|
"git.cacert.org/oidc_login/common/models"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionName = "resource_session"
|
const sessionName = "resource_session"
|
||||||
|
|
||||||
func Authenticate(ctx context.Context, clientId string) func(http.Handler) http.Handler {
|
func Authenticate(ctx context.Context, logger *log.Logger, clientId string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := services.GetSessionStore().Get(r, sessionName)
|
session, err := services.GetSessionStore().Get(r, sessionName)
|
||||||
|
@ -43,6 +47,7 @@ func Authenticate(ctx context.Context, clientId string) func(http.Handler) http.
|
||||||
queryValues.Set("response_type", "code")
|
queryValues.Set("response_type", "code")
|
||||||
queryValues.Set("scope", "openid offline_access profile email")
|
queryValues.Set("scope", "openid offline_access profile email")
|
||||||
queryValues.Set("state", base64.URLEncoding.EncodeToString(commonServices.GenerateKey(8)))
|
queryValues.Set("state", base64.URLEncoding.EncodeToString(commonServices.GenerateKey(8)))
|
||||||
|
queryValues.Set("claims", getRequestedClaims(logger))
|
||||||
authUrl.RawQuery = queryValues.Encode()
|
authUrl.RawQuery = queryValues.Encode()
|
||||||
|
|
||||||
w.Header().Set("Location", authUrl.String())
|
w.Header().Set("Location", authUrl.String())
|
||||||
|
@ -51,6 +56,22 @@ func Authenticate(ctx context.Context, clientId string) func(http.Handler) http.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRequestedClaims(logger *log.Logger) string {
|
||||||
|
claims := make(models.OIDCClaimsRequest)
|
||||||
|
claims["userinfo"] = make(models.ClaimElement)
|
||||||
|
essentialItem := make(models.IndividualClaimRequest)
|
||||||
|
essentialItem["essential"] = true
|
||||||
|
claims["userinfo"]["https://cacert.localhost/groups"] = &essentialItem
|
||||||
|
|
||||||
|
target := make([]byte, 0)
|
||||||
|
buf := bytes.NewBuffer(target)
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
if err := enc.Encode(claims); err != nil {
|
||||||
|
logger.Warnf("could not encode claims request parameter: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
func ParseIdToken(token string, keySet *jwk.Set) (openid.Token, error) {
|
func ParseIdToken(token string, keySet *jwk.Set) (openid.Token, error) {
|
||||||
if parsedIdToken, err := jwt.ParseString(token, jwt.WithKeySet(keySet), jwt.WithOpenIDClaims()); err != nil {
|
if parsedIdToken, err := jwt.ParseString(token, jwt.WithKeySet(keySet), jwt.WithOpenIDClaims()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -87,7 +87,7 @@ func main() {
|
||||||
|
|
||||||
services.InitSessionStore(logger, sessionPath, sessionAuthKey, sessionEncKey)
|
services.InitSessionStore(logger, sessionPath, sessionAuthKey, sessionEncKey)
|
||||||
|
|
||||||
authMiddleware := handlers.Authenticate(ctx, oidcClientId)
|
authMiddleware := handlers.Authenticate(ctx, logger, oidcClientId)
|
||||||
|
|
||||||
serverAddr := fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port"))
|
serverAddr := fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port"))
|
||||||
|
|
||||||
|
|
158
common/models/oidc.go
Normal file
158
common/models/oidc.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
This package contains data models.
|
||||||
|
*/
|
||||||
|
package models
|
||||||
|
|
||||||
|
// An individual claim request.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
|
||||||
|
type IndividualClaimRequest map[string]interface{}
|
||||||
|
|
||||||
|
// ClaimElement represents a claim element
|
||||||
|
type ClaimElement map[string]*IndividualClaimRequest
|
||||||
|
|
||||||
|
// OIDCClaimsRequest the claims request parameter sent with the authorization request.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
|
||||||
|
type OIDCClaimsRequest map[string]ClaimElement
|
||||||
|
|
||||||
|
// GetUserInfo extracts the userinfo claim element from the request.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
|
||||||
|
//
|
||||||
|
// Requests that the listed individual Claims be returned from the UserInfo
|
||||||
|
// Endpoint. If present, the listed Claims are being requested to be added to
|
||||||
|
// any Claims that are being requested using scope values. If not present, the
|
||||||
|
// Claims being requested from the UserInfo Endpoint are only those requested
|
||||||
|
// using scope values.
|
||||||
|
//
|
||||||
|
// When the userinfo member is used, the request MUST also use a response_type
|
||||||
|
// value that results in an Access Token being issued to the Client for use at
|
||||||
|
// the UserInfo Endpoint.
|
||||||
|
func (r OIDCClaimsRequest) GetUserInfo() *ClaimElement {
|
||||||
|
if userInfo, ok := r["userinfo"]; ok {
|
||||||
|
return &userInfo
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDToken extracts the id_token claim element from the request.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
|
||||||
|
//
|
||||||
|
// Requests that the listed individual Claims be returned in the ID Token. If
|
||||||
|
// present, the listed Claims are being requested to be added to the default
|
||||||
|
// Claims in the ID Token. If not present, the default ID Token Claims are
|
||||||
|
// requested, as per the ID Token definition in Section 2 and per the
|
||||||
|
// additional per-flow ID Token requirements in Sections 3.1.3.6, 3.2.2.10,
|
||||||
|
// 3.3.2.11, and 3.3.3.6.
|
||||||
|
func (r OIDCClaimsRequest) GetIDToken() *ClaimElement {
|
||||||
|
if idToken, ok := r["id_token"]; ok {
|
||||||
|
return &idToken
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether the individual claim is an essential claim.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
|
||||||
|
//
|
||||||
|
// Indicates whether the Claim being requested is an Essential Claim. If the
|
||||||
|
// value is true, this indicates that the Claim is an Essential Claim. For
|
||||||
|
// instance, the Claim request:
|
||||||
|
//
|
||||||
|
// "auth_time": {"essential": true}
|
||||||
|
//
|
||||||
|
// can be used to specify that it is Essential to return an auth_time Claim
|
||||||
|
// Value. If the value is false, it indicates that it is a Voluntary Claim.
|
||||||
|
// The default is false.
|
||||||
|
//
|
||||||
|
// By requesting Claims as Essential Claims, the RP indicates to the End-User
|
||||||
|
// that releasing these Claims will ensure a smooth authorization for the
|
||||||
|
// specific task requested by the End-User.
|
||||||
|
//
|
||||||
|
// Note that even if the Claims are not available because the End-User did not
|
||||||
|
// authorize their release or they are not present, the Authorization Server
|
||||||
|
// MUST NOT generate an error when Claims are not returned, whether they are
|
||||||
|
// Essential or Voluntary, unless otherwise specified in the description of
|
||||||
|
// the specific claim.
|
||||||
|
func (i IndividualClaimRequest) IsEssential() bool {
|
||||||
|
if essential, ok := i["essential"]; ok {
|
||||||
|
return essential.(bool)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the wanted value for an individual claim request.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
|
||||||
|
//
|
||||||
|
// Requests that the Claim be returned with a particular value. For instance
|
||||||
|
// the Claim request:
|
||||||
|
//
|
||||||
|
// "sub": {"value": "248289761001"}
|
||||||
|
//
|
||||||
|
// can be used to specify that the request apply to the End-User with Subject
|
||||||
|
// Identifier 248289761001. The value of the value member MUST be a valid
|
||||||
|
// value for the Claim being requested. Definitions of individual Claims can
|
||||||
|
// include requirements on how and whether the value qualifier is to be used
|
||||||
|
// when requesting that Claim.
|
||||||
|
func (i IndividualClaimRequest) WantedValue() *string {
|
||||||
|
if value, ok := i["value"]; ok {
|
||||||
|
valueString := value.(string)
|
||||||
|
return &valueString
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the allowed values for an individual claim request that specifies
|
||||||
|
// a values field.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
|
||||||
|
//
|
||||||
|
// Requests that the Claim be returned with one of a set of values, with the
|
||||||
|
// values appearing in order of preference. For instance the Claim request:
|
||||||
|
//
|
||||||
|
// "acr": {"essential": true,
|
||||||
|
// "values": ["urn:mace:incommon:iap:silver",
|
||||||
|
// "urn:mace:incommon:iap:bronze"]}
|
||||||
|
//
|
||||||
|
// specifies that it is Essential that the acr Claim be returned with either
|
||||||
|
// the value urn:mace:incommon:iap:silver or urn:mace:incommon:iap:bronze.
|
||||||
|
// The values in the values member array MUST be valid values for the Claim
|
||||||
|
// being requested. Definitions of individual Claims can include requirements
|
||||||
|
// on how and whether the values qualifier is to be used when requesting that
|
||||||
|
// Claim.
|
||||||
|
func (i IndividualClaimRequest) AllowedValues() []string {
|
||||||
|
if values, ok := i["values"]; ok {
|
||||||
|
return values.([]string)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenIDConfiguration contains the parts of the OpenID discovery information
|
||||||
|
// that are relevant for us.
|
||||||
|
//
|
||||||
|
// Specification
|
||||||
|
//
|
||||||
|
// See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||||
|
type OpenIDConfiguration struct {
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
JwksUri string `json:"jwks_uri"`
|
||||||
|
EndSessionEndpoint string `json:"end_session_endpoint"`
|
||||||
|
}
|
4
common/services/godoc.go
Normal file
4
common/services/godoc.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*
|
||||||
|
The package services provides services shared by the idp and the app.
|
||||||
|
*/
|
||||||
|
package services
|
|
@ -10,23 +10,20 @@ import (
|
||||||
"github.com/lestrrat-go/jwx/jwk"
|
"github.com/lestrrat-go/jwx/jwk"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"git.cacert.org/oidc_login/common/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type oidcContextKey int
|
type oidcContextKey int
|
||||||
|
|
||||||
|
// context keys
|
||||||
const (
|
const (
|
||||||
ctxOidcConfig oidcContextKey = iota
|
ctxOidcConfig oidcContextKey = iota
|
||||||
ctxOAuth2Config
|
ctxOAuth2Config
|
||||||
ctxOidcJwks
|
ctxOidcJwks
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenIDConfiguration struct {
|
// Parameters for DiscoverOIDC
|
||||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
|
||||||
TokenEndpoint string `json:"token_endpoint"`
|
|
||||||
JwksUri string `json:"jwks_uri"`
|
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcParams struct {
|
type OidcParams struct {
|
||||||
OidcServer string
|
OidcServer string
|
||||||
OidcClientId string
|
OidcClientId string
|
||||||
|
@ -34,6 +31,16 @@ type OidcParams struct {
|
||||||
APIClient *http.Client
|
APIClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Discover 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(ctx context.Context, logger *log.Logger, params *OidcParams) (context.Context, error) {
|
func DiscoverOIDC(ctx context.Context, logger *log.Logger, params *OidcParams) (context.Context, error) {
|
||||||
var discoveryUrl *url.URL
|
var discoveryUrl *url.URL
|
||||||
|
|
||||||
|
@ -60,7 +67,7 @@ func DiscoverOIDC(ctx context.Context, logger *log.Logger, params *OidcParams) (
|
||||||
}
|
}
|
||||||
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
dec := json.NewDecoder(resp.Body)
|
||||||
discoveryResponse := &OpenIDConfiguration{}
|
discoveryResponse := &models.OpenIDConfiguration{}
|
||||||
err = dec.Decode(discoveryResponse)
|
err = dec.Decode(discoveryResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -87,14 +94,23 @@ func DiscoverOIDC(ctx context.Context, logger *log.Logger, params *OidcParams) (
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOidcConfig(ctx context.Context) *OpenIDConfiguration {
|
// Get the OpenID configuration from the context.
|
||||||
return ctx.Value(ctxOidcConfig).(*OpenIDConfiguration)
|
//
|
||||||
|
// DiscoverOIDC needs to be called before this is available.
|
||||||
|
func GetOidcConfig(ctx context.Context) *models.OpenIDConfiguration {
|
||||||
|
return ctx.Value(ctxOidcConfig).(*models.OpenIDConfiguration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the OAuth 2 configuration configuration from the context.
|
||||||
|
//
|
||||||
|
// DiscoverOIDC needs to be called before this is available.
|
||||||
func GetOAuth2Config(ctx context.Context) *oauth2.Config {
|
func GetOAuth2Config(ctx context.Context) *oauth2.Config {
|
||||||
return ctx.Value(ctxOAuth2Config).(*oauth2.Config)
|
return ctx.Value(ctxOAuth2Config).(*oauth2.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the JSON Web Key set from the context.
|
||||||
|
//
|
||||||
|
// DiscoverOIDC needs to be called before this is available.
|
||||||
func GetJwkSet(ctx context.Context) *jwk.Set {
|
func GetJwkSet(ctx context.Context) *jwk.Set {
|
||||||
return ctx.Value(ctxOidcJwks).(*jwk.Set)
|
return ctx.Value(ctxOidcJwks).(*jwk.Set)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,11 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -20,6 +22,7 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"git.cacert.org/oidc_login/common/handlers"
|
"git.cacert.org/oidc_login/common/handlers"
|
||||||
|
models2 "git.cacert.org/oidc_login/common/models"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
"git.cacert.org/oidc_login/idp/services"
|
"git.cacert.org/oidc_login/idp/services"
|
||||||
)
|
)
|
||||||
|
@ -147,10 +150,16 @@ WHERE uniqueid = ?
|
||||||
for _, scope := range consentData.GetPayload().RequestedScope {
|
for _, scope := range consentData.GetPayload().RequestedScope {
|
||||||
switch scope {
|
switch scope {
|
||||||
case "email":
|
case "email":
|
||||||
|
// email
|
||||||
|
// OPTIONAL. This scope value requests access to the email and email_verified Claims.
|
||||||
idTokenData[openid.EmailKey] = userInfo.Email
|
idTokenData[openid.EmailKey] = userInfo.Email
|
||||||
idTokenData[openid.EmailVerifiedKey] = userInfo.EmailVerified
|
idTokenData[openid.EmailVerifiedKey] = userInfo.EmailVerified
|
||||||
break
|
break
|
||||||
case "profile":
|
case "profile":
|
||||||
|
// profile
|
||||||
|
// OPTIONAL. This scope value requests access to the End-User's default profile Claims, which
|
||||||
|
// are: name, family_name, given_name, middle_name, nickname, preferred_username, profile,
|
||||||
|
// picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
|
||||||
idTokenData[openid.GivenNameKey] = userInfo.GivenName
|
idTokenData[openid.GivenNameKey] = userInfo.GivenName
|
||||||
idTokenData[openid.FamilyNameKey] = userInfo.FamilyName
|
idTokenData[openid.FamilyNameKey] = userInfo.FamilyName
|
||||||
idTokenData[openid.MiddleNameKey] = userInfo.MiddleName
|
idTokenData[openid.MiddleNameKey] = userInfo.MiddleName
|
||||||
|
@ -164,6 +173,14 @@ WHERE uniqueid = ?
|
||||||
idTokenData[openid.UpdatedAtKey] = userInfo.Modified.Time.Unix()
|
idTokenData[openid.UpdatedAtKey] = userInfo.Modified.Time.Unix()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "address":
|
||||||
|
// address
|
||||||
|
// OPTIONAL. This scope value requests access to the address Claim.
|
||||||
|
break
|
||||||
|
case "phone":
|
||||||
|
// phone
|
||||||
|
// OPTIONAL. This scope value requests access to the phone_number and phone_number_verified Claims.
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionData := &models.ConsentRequestSession{
|
sessionData := &models.ConsentRequestSession{
|
||||||
|
@ -218,6 +235,21 @@ func (h *consentHandler) renderConsentForm(
|
||||||
return h.messageCatalog.LookupMessage(id, nil, localizer)
|
return h.messageCatalog.LookupMessage(id, nil, localizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestedClaims models2.OIDCClaimsRequest
|
||||||
|
requestUrl, err := url.Parse(consentData.Payload.RequestURL)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warnf("could not parse original request URL %s: %v", consentData.Payload.RequestURL, err)
|
||||||
|
} else {
|
||||||
|
claimsParameter := requestUrl.Query().Get("claims")
|
||||||
|
if claimsParameter != "" {
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(claimsParameter))
|
||||||
|
err := decoder.Decode(&requestedClaims)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warnf("ignoring claims request parameter %s that could not be decoded: %v", claimsParameter, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// render consent form
|
// render consent form
|
||||||
client := consentData.GetPayload().Client
|
client := consentData.GetPayload().Client
|
||||||
err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
|
err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
|
||||||
|
|
Reference in a new issue