Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for CIBA #473

Merged
merged 7 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions authentication/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type Authentication struct {
MFA *MFA
OAuth *OAuth
Passwordless *Passwordless
CIBA *CIBA

auth0ClientInfo *client.Auth0ClientInfo
basePath string
Expand Down Expand Up @@ -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{}

Expand Down
61 changes: 61 additions & 0 deletions authentication/ciba.go
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 22 additions & 0 deletions authentication/ciba/ciba.go
Original file line number Diff line number Diff line change
@@ -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"`
}
85 changes: 85 additions & 0 deletions authentication/ciba_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
33 changes: 33 additions & 0 deletions authentication/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"testing"
"time"

"github.com/auth0/go-auth0/authentication/ciba"

"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/stretchr/testify/assert"
Expand All @@ -18,6 +20,37 @@ import (
"github.com/auth0/go-auth0/authentication/oauth"
)

func TestLoginWithGrant(t *testing.T) {
t.Run("Should return token for CIBA", func(t *testing.T) {
// This test required approval on Guardian MFA application.
// Hence, it cannot be recorded and is only for manual testing.
t.Skip("Skipped as cannot be test in E2E scenario")

// 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",
})

token, err := authAPI.OAuth.LoginWithGrant(context.Background(),
"urn:openid:params:grant-type:ciba",
url.Values{
"auth_req_id": []string{resp.AuthReqID},
"client_id": []string{clientID},
"client_secret": []string{clientSecret},
},
oauth.IDTokenValidationOptions{})
assert.Empty(t, err)
assert.NotEmpty(t, token.AccessToken)
})
}
func TestOAuthLoginWithPassword(t *testing.T) {
auth, err := New(
context.Background(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
version: 2
interactions: []
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions test/data/recordings/authentication/TestCIBA_Initiate.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading