diff --git a/authentication/authentication.go b/authentication/authentication.go index 97087684..c1dc0ba5 100644 --- a/authentication/authentication.go +++ b/authentication/authentication.go @@ -123,6 +123,7 @@ type Authentication struct { MFA *MFA OAuth *OAuth Passwordless *Passwordless + CIBA *CIBA auth0ClientInfo *client.Auth0ClientInfo basePath string @@ -184,6 +185,7 @@ func New(ctx context.Context, domain string, options ...Option) (*Authentication a.MFA = (*MFA)(&a.common) a.OAuth = (*OAuth)(&a.common) a.Passwordless = (*Passwordless)(&a.common) + a.CIBA = (*CIBA)(&a.common) validatorOpts := []idtokenvalidator.Option{} diff --git a/authentication/ciba.go b/authentication/ciba.go new file mode 100644 index 00000000..aca941c0 --- /dev/null +++ b/authentication/ciba.go @@ -0,0 +1,61 @@ +package authentication + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/auth0/go-auth0/authentication/ciba" +) + +// CIBA manager. +type CIBA manager + +// Initiate given a CIBA request params initiates a CIBA authentication request. +// +// This sends a POST request to the /bc-authorize endpoint and returns an auth_req_id for polling. +func (c *CIBA) Initiate(ctx context.Context, body ciba.Request, opts ...RequestOption) (r *ciba.Response, err error) { + if body.ClientID == "" { + body.ClientID = c.authentication.clientID + } + + if body.ClientSecret == "" { + body.ClientSecret = c.authentication.clientSecret + } + + var missing []string + check(&missing, "ClientID", body.ClientID != "" || c.authentication.clientID != "") + check(&missing, "ClientSecret", body.ClientSecret != "" || c.authentication.clientSecret != "") + check(&missing, "LoginHint", len(body.LoginHint) != 0) + check(&missing, "Scope", body.Scope != "") + check(&missing, "BindingMessage", body.BindingMessage != "") + + if len(missing) > 0 { + return nil, fmt.Errorf("missing required fields: %s", strings.Join(missing, ", ")) + } + + data := url.Values{ + "client_id": []string{body.ClientID}, + "client_secret": []string{body.ClientSecret}, + "scope": []string{body.Scope}, + "binding_message": []string{body.BindingMessage}, + } + + jsonBytes, err := json.Marshal(body.LoginHint) + if err != nil { + fmt.Println("Error marshaling map to JSON:", err) + return + } + + data.Set("login_hint", string(jsonBytes)) + + // Perform the request + err = c.authentication.Request(ctx, "POST", c.authentication.URI("bc-authorize"), data, &r, opts...) + if err != nil { + return nil, err + } + + return +} diff --git a/authentication/ciba/ciba.go b/authentication/ciba/ciba.go new file mode 100644 index 00000000..714b4734 --- /dev/null +++ b/authentication/ciba/ciba.go @@ -0,0 +1,22 @@ +package ciba + +// Request defines the request body for calling the bc-authorize endpoint. +type Request struct { + // The client_id of your client. + ClientID string `json:"client_id,omitempty"` + // The client_secret of your client. + ClientSecret string `json:"client_secret,omitempty"` + // This is a required field containing format, iss and sub + LoginHint map[string]string `json:"login_hint,omitempty"` + // The scope for the flow + Scope string `json:"scope,omitempty"` + Audience string `json:"audience,omitempty"` + BindingMessage string `json:"binding_message,omitempty"` +} + +// Response defines the response of the CIBA request. +type Response struct { + AuthReqID string `json:"auth_req_id,omitempty"` + ExpiresIn int64 `json:"expires_in,omitempty"` + Interval int64 `json:"interval,omitempty"` +} diff --git a/authentication/ciba_test.go b/authentication/ciba_test.go new file mode 100644 index 00000000..5ed2d0fe --- /dev/null +++ b/authentication/ciba_test.go @@ -0,0 +1,85 @@ +package authentication + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/auth0/go-auth0/authentication/ciba" +) + +func TestCIBA_Initiate(t *testing.T) { + configureHTTPTestRecordings(t, authAPI) + + // Call the Initiate method of the CIBA manager + resp, err := authAPI.CIBA.Initiate(context.Background(), ciba.Request{ + ClientID: clientID, + ClientSecret: clientSecret, + Scope: "openid", + LoginHint: map[string]string{ + "format": "iss_sub", + "iss": "https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/", + "sub": "auth0|6707939cad3d8bec47ecfa2e", + }, + BindingMessage: "TEST-BINDING-MESSAGE", + }) + + // Validate the response + require.NoError(t, err) + require.NotNil(t, resp) + assert.NotEmpty(t, resp.AuthReqID, "auth_req_id should not be empty") + assert.Greater(t, resp.ExpiresIn, int64(0), "expires_in should be greater than 0") + assert.Greater(t, resp.Interval, int64(0), "interval should be greater than 0") +} + +func TestCIBANegative_Initiate(t *testing.T) { + t.Run("Should throw error for missing LoginHint and BindingMessage", func(t *testing.T) { + configureHTTPTestRecordings(t, authAPI) + + _, err := authAPI.CIBA.Initiate(context.Background(), ciba.Request{ + ClientID: clientID, + ClientSecret: clientSecret, + Scope: "openid", + }) + + assert.ErrorContains(t, err, "missing required fields: LoginHint, BindingMessage") + }) + + t.Run("Should throw error for invalid User ID", func(t *testing.T) { + configureHTTPTestRecordings(t, authAPI) + + _, err := authAPI.CIBA.Initiate(context.Background(), ciba.Request{ + ClientID: clientID, + ClientSecret: clientSecret, + Scope: "openid", + LoginHint: map[string]string{ + "format": "iss_sub", + "iss": "https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/", + "sub": "auth0|Random-ID", + }, + BindingMessage: "TEST-BINDING-MESSAGE", + }) + + assert.ErrorContains(t, err, "User ID is malformed or unknown") + }) + + t.Run("Should throw error if scope is not openid", func(t *testing.T) { + configureHTTPTestRecordings(t, authAPI) + + _, err := authAPI.CIBA.Initiate(context.Background(), ciba.Request{ + ClientID: clientID, + ClientSecret: clientSecret, + Scope: "tempID", + LoginHint: map[string]string{ + "format": "iss_sub", + "iss": "https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/", + "sub": "auth0|6707939cad3d8bec47ecfa2e", + }, + BindingMessage: "TEST-BINDING-MESSAGE", + }) + + assert.ErrorContains(t, err, "openid scope must be requested") + }) +} diff --git a/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_for_invalid_User_ID.yaml b/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_for_invalid_User_ID.yaml new file mode 100644 index 00000000..16be13b7 --- /dev/null +++ b/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_for_invalid_User_ID.yaml @@ -0,0 +1,46 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 350 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: binding_message=TEST-BINDING-MESSAGE&client_id=test-client_id&client_secret=test-client_secret&login_hint=%7B%22format%22%3A%22iss_sub%22%2C%22iss%22%3A%22https%3A%2F%2Fgo-auth0-dev.eu.auth0.com.sus.auth0.com%2F%22%2C%22sub%22%3A%22auth0%7CRandom-ID%22%7D&scope=openid + form: + binding_message: + - TEST-BINDING-MESSAGE + client_id: + - test-client_id + client_secret: + - test-client_secret + login_hint: + - '{"format":"iss_sub","iss":"https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/","sub":"auth0|Random-ID"}' + scope: + - openid + headers: + Content-Type: + - application/x-www-form-urlencoded + url: https://go-auth0-dev.eu.auth0.com/bc-authorize + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: 81 + uncompressed: false + body: '{"error":"unknown_user_id","error_description":"User ID is malformed or unknown"}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 400 Bad Request + code: 400 + duration: 348.832459ms diff --git a/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_for_missing_LoginHint_and_BindingMessage.yaml b/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_for_missing_LoginHint_and_BindingMessage.yaml new file mode 100644 index 00000000..2797c38e --- /dev/null +++ b/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_for_missing_LoginHint_and_BindingMessage.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_if_scope_is_not_openid.yaml b/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_if_scope_is_not_openid.yaml new file mode 100644 index 00000000..1094efec --- /dev/null +++ b/test/data/recordings/authentication/TestCIBANegative_Initiate/Should_throw_error_if_scope_is_not_openid.yaml @@ -0,0 +1,46 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 365 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: binding_message=TEST-BINDING-MESSAGE&client_id=test-client_id&client_secret=test-client_secret&login_hint=%7B%22format%22%3A%22iss_sub%22%2C%22iss%22%3A%22https%3A%2F%2Fgo-auth0-dev.eu.auth0.com.sus.auth0.com%2F%22%2C%22sub%22%3A%22auth0%7C6707939cad3d8bec47ecfa2e%22%7D&scope=tempID + form: + binding_message: + - TEST-BINDING-MESSAGE + client_id: + - test-client_id + client_secret: + - test-client_secret + login_hint: + - '{"format":"iss_sub","iss":"https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/","sub":"auth0|6707939cad3d8bec47ecfa2e"}' + scope: + - tempID + headers: + Content-Type: + - application/x-www-form-urlencoded + url: https://go-auth0-dev.eu.auth0.com/bc-authorize + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: 80 + uncompressed: false + body: '{"error":"invalid_request","error_description":"openid scope must be requested"}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 400 Bad Request + code: 400 + duration: 350.968333ms diff --git a/test/data/recordings/authentication/TestCIBA_Initiate.yaml b/test/data/recordings/authentication/TestCIBA_Initiate.yaml new file mode 100644 index 00000000..c5416a3b --- /dev/null +++ b/test/data/recordings/authentication/TestCIBA_Initiate.yaml @@ -0,0 +1,46 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 365 + transfer_encoding: [] + trailer: {} + host: go-auth0-dev.eu.auth0.com + remote_addr: "" + request_uri: "" + body: binding_message=TEST-BINDING-MESSAGE&client_id=test-client_id&client_secret=test-client_secret&login_hint=%7B%22format%22%3A%22iss_sub%22%2C%22iss%22%3A%22https%3A%2F%2Fgo-auth0-dev.eu.auth0.com.sus.auth0.com%2F%22%2C%22sub%22%3A%22auth0%7C6707939cad3d8bec47ecfa2e%22%7D&scope=openid + form: + binding_message: + - TEST-BINDING-MESSAGE + client_id: + - test-client_id + client_secret: + - test-client_secret + login_hint: + - '{"format":"iss_sub","iss":"https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/","sub":"auth0|6707939cad3d8bec47ecfa2e"}' + scope: + - openid + headers: + Content-Type: + - application/x-www-form-urlencoded + url: https://go-auth0-dev.eu.auth0.com/bc-authorize + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: true + body: '{"auth_req_id":"XyVW8Z3HHmSXMGYUOhNRK6x7Smti6w_Jb4NEXWeR6FNXl-c6OEMp3n5qPrvqGm6M","expires_in":300,"interval":5}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 200 OK + code: 200 + duration: 650.248083ms