Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4f6a4eb
feat(rich-consents): support authorization details
cgcladeraokta Feb 13, 2025
2fc99c1
feat(rich-consents): authz details typed overload
cgcladeraokta Feb 14, 2025
247bc97
fix(rich-consents): get authz details by type
cgcladeraokta Feb 17, 2025
2643cba
feat(app): consent fragments
cgcladeraokta Feb 17, 2025
9c9ed49
feat(app): payment initiation consent
cgcladeraokta Feb 18, 2025
60f9395
doc: document rich consent authorizadion details
cgcladeraokta Feb 18, 2025
110432e
refactor(guardian): guardian rich consent static gson
cgcladeraokta Feb 18, 2025
19f8c22
fix: return empty list instead of null
cgcladeraokta Feb 20, 2025
b5f0dca
refactor: avoid serializing/deserializing using JsonObject
cgcladeraokta Feb 20, 2025
3fa7fb6
refactor(guardian): authorization details type annotation
cgcladeraokta Feb 21, 2025
a2ce1a9
refactor: filter authorization details by type
cgcladeraokta Feb 26, 2025
e7642d8
feat: payment initiation actions and locations
cgcladeraokta Feb 26, 2025
0010268
feat: render dynamic authorization details item
cgcladeraokta Feb 26, 2025
75655b3
refactor: improve code
cgcladeraokta Feb 26, 2025
c42f8b4
doc: improve authz details docs in README
cgcladeraokta Feb 27, 2025
67ad9c8
refactor: fix payment details object definition
cgcladeraokta Feb 27, 2025
b7f56a8
fix: do not render regular authentication consent if linkingid
cgcladeraokta Feb 27, 2025
1f877c5
fix: improve resources organization
cgcladeraokta Feb 27, 2025
a7a1651
refactor: improve code readability
cgcladeraokta Feb 27, 2025
d37cb1c
fix: invalid display theme
cgcladeraokta Feb 27, 2025
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
76 changes: 68 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,17 @@ The `deviceName` and `fcmToken` are data that you must provide:
#### A note about key generation

The Guardian SDK does not provide methods for generating and storing cryptographic keys used for enrollment
as this is an application specific concern and could vary between targeted versions of Android and
OEM-specific builds. The example given above and that used in the sample application is a naive implementation
as this is an application specific concern and could vary between targeted versions of Android and
OEM-specific builds. The example given above and that used in the sample application is a naive implementation
which may not be suitable for production applications. It is recommended that you follow [OWASP guidelines
for Android Cryptographic APIs](https://mas.owasp.org/MASTG/0x05e-Testing-Cryptography/) for your implementation.

As of version 0.9.0 the public key used for enrollment was added to the Enrollment Interface as it is
required for [fetching rich-consent details](#fetch-rich-consent-details). For new installs,
this is not a a concern. For enrollments created prior to this version, depending on implementation,
this key may or may not have been stored with the enrollment information. If this key was discarded,
it may be possible to reconstruct from the stored signing key. The sample app provides
[an example](app/src/main/java/com/auth0/guardian/sample/ParcelableEnrollment.java#L188) of this. If
this is not a a concern. For enrollments created prior to this version, depending on implementation,
this key may or may not have been stored with the enrollment information. If this key was discarded,
it may be possible to reconstruct from the stored signing key. The sample app provides
[an example](app/src/main/java/com/auth0/guardian/sample/ParcelableEnrollment.java#L188) of this. If
this is not possible, devices will require re-enrollment to make use of this functionality.

### Unenroll
Expand Down Expand Up @@ -208,7 +208,7 @@ if (notification.getTransctionLinkingId() != null) {
.start(new Callback<Enrollment> {
@Override
void onSuccess(RichConsent consentDetails) {
// we have the consent details
// we have the consent details
}

@Override
Expand All @@ -219,12 +219,72 @@ if (notification.getTransctionLinkingId() != null) {
// there is no consent associated with the transaction
}
}
// something went wrong
// something went wrong
}
});
}
```

#### Authorization Details

If Rich Authorization Rich Authorization Requests are being used, the consent record will contains the `authorization_details` values from the initial authenication request ([RFC 9396](https://datatracker.ietf.org/doc/html/rfc9396)) for rendering to the user for consent. You can access them in the `getAuthorizationDetails()` method of the requested details object which returns an array of objects containing each of the types. `authorization_details` values are essentially arbitary JSON objects but are guaranteed to have a `type` property which must be pre-registered with the Authorization Server. As such the can be queried in a dynamic manor like you might with JSON.

```java
void onSuccess(RichConsent consentDetails) {
List<Map<String, Object>> authorizationDetails = consentDetails
.getRequestedDetails()
.getAuthorizationDetails();

String type = (String) authorizationDetails.get(0).get("type");
int amount = (int) authorizationDetails.get(0).get("amount");
}
```
Typically the shape and type of `authorization_details` will be known at compile time. In such a case, `authorization_details` can be queried in a strongly-typed manor by first defining a class decorated with `@AuthorizationDetailsType("<type>")` to represent your object and making use of the `filterAuthorizationDetailsByType` helper function, which will return all authorization details that match this type.

Guardian SDK uses Gson for desiariliazing JSON API responses. Although, your app is not required to depend on Gson directly, the Authorization Details Type classes you define must be compatible with Gson's [Objects deserialization rules](https://github.com/google/gson/blob/main/UserGuide.md#object-examples).

```java
@AuthorizationDetailsType("payment")
class PaymentDetails {
private final String type;
private final int amount;
private final String currency;

public MyAuthorizationDetails(String type, int amount, Strinc currencty) {
this.type = type;
this.amount = amount;
this.currency = currency;
}

public String getType() {
return type;
}

public int getAmount() {
return amount;
}

public String getCurrency() {
return currency;
}
}


void onSuccess(RichConsent consentDetails) {
List<PaymentDetails> authorizationDetails = consentDetails
.getRequestedDetails()
.filterAuthorizationDetailsByType(PaymentDetails.class);

PaymentDetails firstPaymentDetails = authorizationDetails.get(0);

int amount = firstPaymentDetails.getAmount();
String currencty = firstPaymentDetails.getCurrency();
}
```

> [!WARNING]
> When using the filter helper, you still need to check if there are other authorization details in the rich consent record to prevent the user giving consent to something they didn't see rendered.

## What is Auth0?

Auth0 helps you to:
Expand Down
36 changes: 1 addition & 35 deletions app/src/main/java/com/auth0/guardian/sample/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,41 +227,7 @@ private void updateEnrollment(ParcelableEnrollment enrollment) {
}

private void onPushNotificationReceived(ParcelableNotification notification) {
Context context = this;
Intent standardNotificationActivityIntent = NotificationActivity.getStartIntent(context, notification, enrollment);

if (notification.getTransactionLinkingId() == null) {
startActivity(standardNotificationActivityIntent);
} else {
try {
guardian.fetchConsent(notification, enrollment).start(new Callback<RichConsent>() {
@Override
public void onSuccess(RichConsent consent) {
Intent intent = NotificationWithConsentDetailsActivity.getStartIntent(
context,
notification,
enrollment,
new ParcelableRichConsent(consent)
);
startActivity(intent);
}

@Override
public void onFailure(Throwable exception) {
if (exception instanceof GuardianException) {
GuardianException guardianException = (GuardianException) exception;
if (guardianException.isResourceNotFound()) {
startActivity(standardNotificationActivityIntent);
}
}
Log.e(TAG, "Error obtaining consent details", exception);

}
});
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
Log.e(TAG, "Error requesting consent details", e);
}
}
startActivity(NotificationActivity.getStartIntent(this, notification, enrollment));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better

}

@Override
Expand Down
136 changes: 114 additions & 22 deletions app/src/main/java/com/auth0/guardian/sample/NotificationActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,39 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

import com.auth0.android.guardian.sdk.Guardian;
import com.auth0.android.guardian.sdk.GuardianException;
import com.auth0.android.guardian.sdk.ParcelableNotification;
import com.auth0.android.guardian.sdk.RichConsent;
import com.auth0.android.guardian.sdk.networking.Callback;
import com.auth0.guardian.sample.fragments.AuthenticationRequestDetailsFragment;
import com.auth0.guardian.sample.fragments.consent.ConsentBasicDetailsFragment;
import com.auth0.guardian.sample.fragments.consent.ConsentPaymentInitiationFragment;
import com.auth0.guardian.sample.fragments.consent.DynamicAuthorizationDetailsFragment;
import com.auth0.guardian.sample.consent.authorization.details.payments.PaymentInitiationDetails;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.List;

public class NotificationActivity extends AppCompatActivity {

private TextView userText;
private TextView browserText;
private TextView osText;
private TextView locationText;
private TextView dateText;
private static final String TAG = NotificationActivity.class.getName();

private Guardian guardian;
private ParcelableEnrollment enrollment;
private ParcelableNotification notification;

private RichConsent richConsent;

static Intent getStartIntent(@NonNull Context context,
@NonNull ParcelableNotification notification,
@NonNull ParcelableEnrollment enrollment) {
Expand Down Expand Up @@ -79,16 +90,39 @@ protected void onCreate(Bundle savedInstanceState) {

setupUI();

updateUI();
if (notification.getTransactionLinkingId() != null) {
Copy link
Contributor

@sam-muncke sam-muncke Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much nicer than having the separate activities with all the logic in Main. This can probably be tidied up a bit though but adding so private methods to reduce the nesting a bit. You also have 3 code paths that all ultimately call updateUI() - pretty sure you can simplify

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved fragment initialization logic bits to private methods. About the three paths calling updateUI(), I did find a bug that caused the regular authentication fragment to always be shown before fetching the rich consent record. However, because it's "async" I didn't find an easy way to simplify it. If you've got ideas I'm all ears.

The bug has been fixed in b7f56a8

try {
guardian.fetchConsent(notification, enrollment).start(new Callback<RichConsent>() {
@Override
public void onSuccess(RichConsent response) {
richConsent = response;
updateUI();
}

@Override
public void onFailure(Throwable exception) {
if (exception instanceof GuardianException) {
GuardianException guardianException = (GuardianException) exception;
if (guardianException.isResourceNotFound()) {
// Render regular authentication request details
updateUI();
}
} else {
Log.e(TAG, "Error requesting consent details", exception);
throw new RuntimeException(exception);
}
}
});
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
} else {
updateUI();
}
}

private void setupUI() {
userText = (TextView) findViewById(R.id.userText);
browserText = (TextView) findViewById(R.id.browserText);
osText = (TextView) findViewById(R.id.osText);
locationText = (TextView) findViewById(R.id.locationText);
dateText = (TextView) findViewById(R.id.dateText);

// TODO: spinner fragment
Button rejectButton = (Button) findViewById(R.id.rejectButton);
assert rejectButton != null;
rejectButton.setOnClickListener(new View.OnClickListener() {
Expand All @@ -109,17 +143,75 @@ public void onClick(View v) {
}

private void updateUI() {
userText.setText(enrollment.getUserId());
browserText.setText(
Fragment fragment;
if (richConsent == null) {
fragment = getStandardAuthenticationFragment();
} else if (richConsent.getRequestedDetails().getAuthorizationDetails().isEmpty()) {
fragment = getBasicConsentFragment();
} else {
List<PaymentInitiationDetails> paymentInitiationDetailsList = richConsent
.getRequestedDetails()
.filterAuthorizationDetailsByType(PaymentInitiationDetails.class);
if (!paymentInitiationDetailsList.isEmpty()) {
// For simplicity, in this example we render one single type
fragment = getPaymentInitiationConsentFragment(paymentInitiationDetailsList.get(0));
} else {
fragment = getDynamicAuthorizationDetailsConsentFragment();
}

}

getSupportFragmentManager().beginTransaction()
.replace(R.id.authenticationDetailsFragmentContainer, fragment)
.commit();

}

@NonNull
private DynamicAuthorizationDetailsFragment getDynamicAuthorizationDetailsConsentFragment() {
return DynamicAuthorizationDetailsFragment.newInstance(
richConsent.getRequestedDetails().getBindingMessage(),
notification.getDate().toString(),
// For simplicity, in this example we render one single type
richConsent.getRequestedDetails().getAuthorizationDetails().get(0)
);
}

@NonNull
private ConsentPaymentInitiationFragment getPaymentInitiationConsentFragment(PaymentInitiationDetails paymentDetails) {
return ConsentPaymentInitiationFragment.newInstance(
richConsent.getRequestedDetails().getBindingMessage(),
paymentDetails.getRemittanceInformation(),
paymentDetails.getCreditorAccount().getAccountNumber(),
paymentDetails.getInstructedAmount().getCurrency(),
paymentDetails.getInstructedAmount().getAmount()
);
}

@NonNull
private ConsentBasicDetailsFragment getBasicConsentFragment() {
return ConsentBasicDetailsFragment.newInstance(
richConsent.getRequestedDetails().getBindingMessage(),
richConsent.getRequestedDetails().getScope(),
notification.getDate().toString()
);
}

@NonNull
private AuthenticationRequestDetailsFragment getStandardAuthenticationFragment() {
return AuthenticationRequestDetailsFragment.newInstance(
enrollment.getUserId(),

String.format("%s, %s",
notification.getBrowserName(),
notification.getBrowserVersion()));
osText.setText(
notification.getBrowserVersion()),
String.format("%s, %s",
notification.getOsName(),
notification.getOsVersion()));
locationText.setText(notification.getLocation());
dateText.setText(notification.getDate().toString());
notification.getOsVersion()),

notification.getLocation(),
notification.getDate().toString()
);
}

private void rejectRequested() {
Expand Down
Loading