Skip to content

Commit

Permalink
Support for CIBA (#473)
Browse files Browse the repository at this point in the history
  • Loading branch information
duedares-rvj authored Jan 8, 2025
2 parents 6452713 + 1519663 commit 0090965
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 0 deletions.
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

0 comments on commit 0090965

Please sign in to comment.