Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
66 changes: 58 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,62 @@ if (notification.getTransctionLinkingId() != null) {
// there is no consent associated with the transaction
}
}
// something went wrong
// something went wrong
}
});
}
```

#### Authorization Details

If the received record contains authorization details ([RFC 9396](https://datatracker.ietf.org/doc/html/rfc9396)) you can access them at `getAuthoriationDetails()` in the requested details object.
It returns a generic `List` of `Map` objects.

```java
void onSuccess(Rich 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");
...
}
```

Or, if you prefer to work with typed objects, you can pass a type key and a class.

```java
class PaymentDetails {
private String type;
private int amount;

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

public String getType() {
return type;
}

public int getAmount() {
return amount;
}
}

...

void onSuccess(Rich consentDetails) {
List<PaymentDetails> authorizationDetails = consentDetails
.getRequestedDetails()
.getAuthorizationDetails("payment", PaymentDetails.class);

int amount = authorizationDetails.get(0).getAmount();
...
}
```

## 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
113 changes: 88 additions & 25 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,38 @@
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.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 +89,38 @@ protected void onCreate(Bundle savedInstanceState) {

setupUI();

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);
}
}
});
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}

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 +141,48 @@ public void onClick(View v) {
}

private void updateUI() {
userText.setText(enrollment.getUserId());
browserText.setText(
String.format("%s, %s",
notification.getBrowserName(),
notification.getBrowserVersion()));
osText.setText(
String.format("%s, %s",
notification.getOsName(),
notification.getOsVersion()));
locationText.setText(notification.getLocation());
dateText.setText(notification.getDate().toString());
Fragment fragment;
if (richConsent == null) {
fragment = AuthenticationRequestDetailsFragment.newInstance(
enrollment.getUserId(),

String.format("%s, %s",
notification.getBrowserName(),
notification.getBrowserVersion()),
String.format("%s, %s",
notification.getOsName(),
notification.getOsVersion()),

notification.getLocation(),
notification.getDate().toString()
);
} else {
List<PaymentInitiationDetails> paymentInitiationDetailsList = richConsent
.getRequestedDetails()
.getAuthorizationDetails("payment_initiation", PaymentInitiationDetails.class);

if (paymentInitiationDetailsList.isEmpty()) {
fragment = ConsentBasicDetailsFragment.newInstance(
richConsent.getRequestedDetails().getBindingMessage(),
richConsent.getRequestedDetails().getScope(),
notification.getDate().toString()
);
} else {
PaymentInitiationDetails paymentDetails = paymentInitiationDetailsList.get(0);
fragment = ConsentPaymentInitiationFragment.newInstance(
richConsent.getRequestedDetails().getBindingMessage(),
paymentDetails.getRemittanceInformation(),
paymentDetails.getCreditorAccount().getAccountNumber(),
paymentDetails.getInstructedAmount().getCurrency(),
paymentDetails.getInstructedAmount().getAmount()
);
}
}

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

}

private void rejectRequested() {
Expand Down
Loading