feat(oidc): OIDC Prompt (#4053)
Boundary OIDC method does not currently support passing in prompts during authentication. This change adds the capability of passing OIDC prompts. Prompts are optional OIDC parameters that determine the behaviour of the authentication server: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest ## Changes - New `auth_oidc_prompt` table which contains all the prompts for OIDC auth method - New `auth_oidc_prompt_enm` table which contains possible enum values for a prompt - Currently supported: - `none`: The Authorization Server MUST NOT display any authentication or consent user interface pages - `login`: The Authorization Server SHOULD prompt the End-User for reauthentication - `consent`: The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client - `select_account`: The Authorization Server SHOULD prompt the End-User to select a user account - `oidc_auth_method_with_value_obj` view has been updated to return `prompt` value - Add `prompt` option for OIDC auth method CLI create and update - Pass configured prompt during OIDC authentication - Add `prompt` API validation for create and update
This commit is contained in:
1 parent
b6df5693b5
commit
8b8d2822df
31 files changed
+1668
-345
No files matched your search
@@ -862,6 +862,7 @@ func toAuthMethodProto(ctx context.Context, in auth.AuthMethod, opt ...handlers.
|
||||
AllowedAudiences: i.GetAudClaims(),
|
||||
ClaimsScopes: i.GetClaimsScopes(),
|
||||
AccountClaimMaps: i.GetAccountClaimMaps(),
|
||||
Prompts: i.GetPrompts(),
|
||||
}
|
||||
if i.DisableDiscoveredConfigValidation {
|
||||
attrs.DisableDiscoveredConfigValidation = true
|
||||
@@ -1031,6 +1032,14 @@ func validateCreateRequest(ctx context.Context, req *pbs.CreateAuthMethodRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(attrs.GetPrompts()) > 0 {
|
||||
for _, p := range attrs.GetPrompts() {
|
||||
if !oidc.SupportedPrompt(oidc.PromptParam(p)) {
|
||||
badFields[promptsField] = fmt.Sprintf("Contains unsupported prompt %q", p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(attrs.GetApiUrlPrefix().GetValue()) == "" {
|
||||
// TODO: When we start accepting the address used in the request make this an optional field.
|
||||
badFields[apiUrlPrefixField] = "This field is required."
|
||||
@@ -1159,6 +1168,14 @@ func validateUpdateRequest(ctx context.Context, req *pbs.UpdateAuthMethodRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(attrs.GetPrompts()) > 0 {
|
||||
for _, p := range attrs.GetPrompts() {
|
||||
if !oidc.SupportedPrompt(oidc.PromptParam(p)) {
|
||||
badFields[promptsField] = fmt.Sprintf("Contains unsupported prompt %q", p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(attrs.GetIdpCaCerts()) > 0 {
|
||||
if _, err := oidc.ParseCertificates(ctx, attrs.GetIdpCaCerts()...); err != nil {
|
||||
badFields[idpCaCertsField] = fmt.Sprintf("Cannot parse CA certificates. %v", err.Error())
|
||||
|
||||
@@ -143,7 +143,8 @@ func TestGet(t *testing.T) {
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.InactiveState, "alice_rp", "secret",
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]),
|
||||
oidc.WithPrompts(oidc.SelectAccount))
|
||||
|
||||
wantOidc := &pb.AuthMethod{
|
||||
Id: oidcam.GetPublicId(),
|
||||
@@ -159,6 +160,7 @@ func TestGet(t *testing.T) {
|
||||
State: string(oidc.InactiveState),
|
||||
ApiUrlPrefix: wrapperspb.String("https://api.com"),
|
||||
CallbackUrl: fmt.Sprintf(oidc.CallbackEndpoint, "https://api.com"),
|
||||
Prompts: []string{string(oidc.SelectAccount)},
|
||||
},
|
||||
},
|
||||
Version: 1,
|
||||
@@ -303,7 +305,8 @@ func TestList(t *testing.T) {
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), oWithAuthMethods.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, oWithAuthMethods.GetPublicId(), oidc.ActivePublicState, "alice_rp", "secret",
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]), oidc.WithSigningAlgs(oidc.EdDSA))
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]), oidc.WithSigningAlgs(oidc.EdDSA),
|
||||
oidc.WithPrompts(oidc.Consent))
|
||||
iam.TestSetPrimaryAuthMethod(t, iamRepo, oWithAuthMethods, oidcam.GetPublicId())
|
||||
|
||||
wantSomeAuthMethods = append(wantSomeAuthMethods, &pb.AuthMethod{
|
||||
@@ -325,6 +328,7 @@ func TestList(t *testing.T) {
|
||||
SigningAlgorithms: []string{
|
||||
string(oidc.EdDSA),
|
||||
},
|
||||
Prompts: []string{string(oidc.Consent)},
|
||||
},
|
||||
},
|
||||
IsPrimary: true,
|
||||
@@ -533,7 +537,8 @@ func TestDelete(t *testing.T) {
|
||||
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.InactiveState, "alice_rp", "my-dogs-name",
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]),
|
||||
oidc.WithPrompts(oidc.SelectAccount))
|
||||
|
||||
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"})
|
||||
|
||||
@@ -1368,6 +1373,66 @@ func TestCreate(t *testing.T) {
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "invalid attributes.account_attribute_maps (unable to parse)",
|
||||
},
|
||||
{
|
||||
name: "OIDC AuthMethod With Unsupported Prompt",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: oidc.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
|
||||
ApiUrlPrefix: wrapperspb.String("https://api.com"),
|
||||
Issuer: wrapperspb.String("https://example2.discovery.url:4821"),
|
||||
ClientId: wrapperspb.String("someclientid"),
|
||||
ClientSecret: wrapperspb.String("secret"),
|
||||
Prompts: []string{string(oidc.SelectAccount), "invalid"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "Contains unsupported prompt",
|
||||
},
|
||||
{
|
||||
name: "Create OIDC AuthMethod With Supported Prompt",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: oidc.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
|
||||
Issuer: wrapperspb.String("https://example.discovery.url:4821/.well-known/openid-configuration/"),
|
||||
ClientId: wrapperspb.String("exampleclientid"),
|
||||
ClientSecret: wrapperspb.String("secret"),
|
||||
ApiUrlPrefix: wrapperspb.String("https://callback.prefix:9281/path"),
|
||||
Prompts: []string{string(oidc.SelectAccount)},
|
||||
},
|
||||
},
|
||||
}},
|
||||
idPrefix: globals.OidcAuthMethodPrefix + "_",
|
||||
res: &pbs.CreateAuthMethodResponse{
|
||||
Uri: fmt.Sprintf("auth-methods/%s_", globals.OidcAuthMethodPrefix),
|
||||
Item: &pb.AuthMethod{
|
||||
Id: defaultAm.GetPublicId(),
|
||||
ScopeId: o.GetPublicId(),
|
||||
CreatedTime: defaultAm.GetCreateTime().GetTimestamp(),
|
||||
UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(),
|
||||
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()},
|
||||
Version: 1,
|
||||
Type: oidc.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
|
||||
Issuer: wrapperspb.String("https://example.discovery.url:4821/"),
|
||||
ClientId: wrapperspb.String("exampleclientid"),
|
||||
ClientSecretHmac: "<hmac>",
|
||||
State: string(oidc.InactiveState),
|
||||
ApiUrlPrefix: wrapperspb.String("https://callback.prefix:9281/path"),
|
||||
CallbackUrl: "https://callback.prefix:9281/path/v1/auth-methods/oidc:authenticate:callback",
|
||||
Prompts: []string{string(oidc.SelectAccount)},
|
||||
},
|
||||
},
|
||||
AuthorizedActions: oidcAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -46,6 +46,7 @@ const (
|
||||
codeField = "attributes.code"
|
||||
claimsScopesField = "attributes.claims_scopes"
|
||||
accountClaimMapsField = "attributes.account_claim_maps"
|
||||
promptsField = "attributes.prompts"
|
||||
)
|
||||
|
||||
var oidcMaskManager handlers.MaskManager
|
||||
@@ -445,6 +446,13 @@ func toStorageOidcAuthMethod(ctx context.Context, scopeId string, in *pb.AuthMet
|
||||
if len(signAlgs) > 0 {
|
||||
opts = append(opts, oidc.WithSigningAlgs(signAlgs...))
|
||||
}
|
||||
var prompts []oidc.PromptParam
|
||||
for _, a := range attrs.GetPrompts() {
|
||||
prompts = append(prompts, oidc.PromptParam(a))
|
||||
}
|
||||
if len(prompts) > 0 {
|
||||
opts = append(opts, oidc.WithPrompts(prompts...))
|
||||
}
|
||||
if len(attrs.GetAllowedAudiences()) > 0 {
|
||||
opts = append(opts, oidc.WithAudClaims(attrs.GetAllowedAudiences()...))
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ func getSetup(t *testing.T) setup {
|
||||
oidc.WithApiUrl(oidc.TestConvertToUrls(t, ret.testController.URL)[0]),
|
||||
oidc.WithSigningAlgs(oidc.Alg(ret.testProviderAlg)),
|
||||
oidc.WithCertificates(ret.testProviderCaCert...),
|
||||
oidc.WithPrompts(oidc.Consent),
|
||||
)
|
||||
|
||||
ret.testProviderAllowedRedirect = fmt.Sprintf(oidc.CallbackEndpoint, ret.testController.URL)
|
||||
@@ -281,6 +282,7 @@ func TestUpdate_OIDC(t *testing.T) {
|
||||
tpClientSecret := "her-dog's-name"
|
||||
tp.SetClientCreds(tpClientId, tpClientSecret)
|
||||
_, _, tpAlg, _ := tp.SigningKeys()
|
||||
tpPrompt := capoidc.None
|
||||
|
||||
defaultAttributes := &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
|
||||
@@ -290,6 +292,7 @@ func TestUpdate_OIDC(t *testing.T) {
|
||||
ApiUrlPrefix: wrapperspb.String("https://example.com"),
|
||||
IdpCaCerts: []string{tp.CACert()},
|
||||
SigningAlgorithms: []string{string(tpAlg)},
|
||||
Prompts: []string{string(tpPrompt)},
|
||||
},
|
||||
}
|
||||
defaultReadAttributes := &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
@@ -302,6 +305,7 @@ func TestUpdate_OIDC(t *testing.T) {
|
||||
CallbackUrl: "https://example.com/v1/auth-methods/oidc:authenticate:callback",
|
||||
IdpCaCerts: []string{tp.CACert()},
|
||||
SigningAlgorithms: []string{string(tpAlg)},
|
||||
Prompts: []string{string(tpPrompt)},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -995,6 +999,53 @@ func TestUpdate_OIDC(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unsupported Prompts",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.prompts"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
|
||||
Prompts: []string{string("invalid")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
},
|
||||
{
|
||||
name: "Update Prompt With Valid Values",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.prompts"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: &pb.OidcAuthMethodAttributes{
|
||||
Prompts: []string{string(oidc.Consent), string(oidc.SelectAccount)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: oidc.Subtype.String(),
|
||||
Attrs: func() *pb.AuthMethod_OidcAuthMethodsAttributes {
|
||||
f := proto.Clone(defaultReadAttributes.OidcAuthMethodsAttributes).(*pb.OidcAuthMethodAttributes)
|
||||
f.Prompts = []string{string(oidc.Consent), string(oidc.SelectAccount)}
|
||||
return &pb.AuthMethod_OidcAuthMethodsAttributes{OidcAuthMethodsAttributes: f}
|
||||
}(),
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: oidcAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user