Skip to content

Commit 8e83c0f

Browse files
feat(guardian): rich consent supports authorization details (#126)
* feat(rich-consents): support authorization details Add support to `authorization_details` property in the Rich Consent record. * feat(rich-consents): authz details typed overload * fix(rich-consents): get authz details by type * feat(app): consent fragments * feat(app): payment initiation consent * doc: document rich consent authorizadion details * refactor(guardian): guardian rich consent static gson * fix: return empty list instead of null * refactor: avoid serializing/deserializing using JsonObject * refactor(guardian): authorization details type annotation * refactor: filter authorization details by type * feat: payment initiation actions and locations * feat: render dynamic authorization details item * refactor: improve code * doc: improve authz details docs in README * refactor: fix payment details object definition * fix: do not render regular authentication consent if linkingid * fix: improve resources organization * refactor: improve code readability * fix: invalid display theme
1 parent ff011fd commit 8e83c0f

29 files changed

Lines changed: 1299 additions & 587 deletions

README.md

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,17 @@ The `deviceName` and `fcmToken` are data that you must provide:
122122
#### A note about key generation
123123

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

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

138138
### Unenroll
@@ -208,7 +208,7 @@ if (notification.getTransctionLinkingId() != null) {
208208
.start(new Callback<Enrollment> {
209209
@Override
210210
void onSuccess(RichConsent consentDetails) {
211-
// we have the consent details
211+
// we have the consent details
212212
}
213213

214214
@Override
@@ -219,12 +219,72 @@ if (notification.getTransctionLinkingId() != null) {
219219
// there is no consent associated with the transaction
220220
}
221221
}
222-
// something went wrong
222+
// something went wrong
223223
}
224224
});
225225
}
226226
```
227227

228+
#### Authorization Details
229+
230+
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.
231+
232+
```java
233+
void onSuccess(RichConsent consentDetails) {
234+
List<Map<String, Object>> authorizationDetails = consentDetails
235+
.getRequestedDetails()
236+
.getAuthorizationDetails();
237+
238+
String type = (String) authorizationDetails.get(0).get("type");
239+
int amount = (int) authorizationDetails.get(0).get("amount");
240+
}
241+
```
242+
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.
243+
244+
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).
245+
246+
```java
247+
@AuthorizationDetailsType("payment")
248+
class PaymentDetails {
249+
private final String type;
250+
private final int amount;
251+
private final String currency;
252+
253+
public MyAuthorizationDetails(String type, int amount, Strinc currencty) {
254+
this.type = type;
255+
this.amount = amount;
256+
this.currency = currency;
257+
}
258+
259+
public String getType() {
260+
return type;
261+
}
262+
263+
public int getAmount() {
264+
return amount;
265+
}
266+
267+
public String getCurrency() {
268+
return currency;
269+
}
270+
}
271+
272+
273+
void onSuccess(RichConsent consentDetails) {
274+
List<PaymentDetails> authorizationDetails = consentDetails
275+
.getRequestedDetails()
276+
.filterAuthorizationDetailsByType(PaymentDetails.class);
277+
278+
PaymentDetails firstPaymentDetails = authorizationDetails.get(0);
279+
280+
int amount = firstPaymentDetails.getAmount();
281+
String currencty = firstPaymentDetails.getCurrency();
282+
}
283+
```
284+
285+
> [!WARNING]
286+
> 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.
287+
228288
## What is Auth0?
229289

230290
Auth0 helps you to:

app/src/main/java/com/auth0/guardian/sample/MainActivity.java

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -227,41 +227,7 @@ private void updateEnrollment(ParcelableEnrollment enrollment) {
227227
}
228228

229229
private void onPushNotificationReceived(ParcelableNotification notification) {
230-
Context context = this;
231-
Intent standardNotificationActivityIntent = NotificationActivity.getStartIntent(context, notification, enrollment);
232-
233-
if (notification.getTransactionLinkingId() == null) {
234-
startActivity(standardNotificationActivityIntent);
235-
} else {
236-
try {
237-
guardian.fetchConsent(notification, enrollment).start(new Callback<RichConsent>() {
238-
@Override
239-
public void onSuccess(RichConsent consent) {
240-
Intent intent = NotificationWithConsentDetailsActivity.getStartIntent(
241-
context,
242-
notification,
243-
enrollment,
244-
new ParcelableRichConsent(consent)
245-
);
246-
startActivity(intent);
247-
}
248-
249-
@Override
250-
public void onFailure(Throwable exception) {
251-
if (exception instanceof GuardianException) {
252-
GuardianException guardianException = (GuardianException) exception;
253-
if (guardianException.isResourceNotFound()) {
254-
startActivity(standardNotificationActivityIntent);
255-
}
256-
}
257-
Log.e(TAG, "Error obtaining consent details", exception);
258-
259-
}
260-
});
261-
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
262-
Log.e(TAG, "Error requesting consent details", e);
263-
}
264-
}
230+
startActivity(NotificationActivity.getStartIntent(this, notification, enrollment));
265231
}
266232

267233
@Override

app/src/main/java/com/auth0/guardian/sample/NotificationActivity.java

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,39 @@
2626
import android.content.Intent;
2727
import android.net.Uri;
2828
import android.os.Bundle;
29-
import androidx.annotation.NonNull;
30-
import androidx.appcompat.app.AppCompatActivity;
29+
import android.util.Log;
3130
import android.view.View;
3231
import android.widget.Button;
33-
import android.widget.TextView;
32+
33+
import androidx.annotation.NonNull;
34+
import androidx.appcompat.app.AppCompatActivity;
35+
import androidx.fragment.app.Fragment;
3436

3537
import com.auth0.android.guardian.sdk.Guardian;
38+
import com.auth0.android.guardian.sdk.GuardianException;
3639
import com.auth0.android.guardian.sdk.ParcelableNotification;
40+
import com.auth0.android.guardian.sdk.RichConsent;
3741
import com.auth0.android.guardian.sdk.networking.Callback;
42+
import com.auth0.guardian.sample.fragments.AuthenticationRequestDetailsFragment;
43+
import com.auth0.guardian.sample.fragments.consent.ConsentBasicDetailsFragment;
44+
import com.auth0.guardian.sample.fragments.consent.ConsentPaymentInitiationFragment;
45+
import com.auth0.guardian.sample.fragments.consent.DynamicAuthorizationDetailsFragment;
46+
import com.auth0.guardian.sample.consent.authorization.details.payments.PaymentInitiationDetails;
47+
48+
import java.security.NoSuchAlgorithmException;
49+
import java.security.spec.InvalidKeySpecException;
50+
import java.util.List;
3851

3952
public class NotificationActivity extends AppCompatActivity {
4053

41-
private TextView userText;
42-
private TextView browserText;
43-
private TextView osText;
44-
private TextView locationText;
45-
private TextView dateText;
54+
private static final String TAG = NotificationActivity.class.getName();
4655

4756
private Guardian guardian;
4857
private ParcelableEnrollment enrollment;
4958
private ParcelableNotification notification;
5059

60+
private RichConsent richConsent;
61+
5162
static Intent getStartIntent(@NonNull Context context,
5263
@NonNull ParcelableNotification notification,
5364
@NonNull ParcelableEnrollment enrollment) {
@@ -79,16 +90,39 @@ protected void onCreate(Bundle savedInstanceState) {
7990

8091
setupUI();
8192

82-
updateUI();
93+
if (notification.getTransactionLinkingId() != null) {
94+
try {
95+
guardian.fetchConsent(notification, enrollment).start(new Callback<RichConsent>() {
96+
@Override
97+
public void onSuccess(RichConsent response) {
98+
richConsent = response;
99+
updateUI();
100+
}
101+
102+
@Override
103+
public void onFailure(Throwable exception) {
104+
if (exception instanceof GuardianException) {
105+
GuardianException guardianException = (GuardianException) exception;
106+
if (guardianException.isResourceNotFound()) {
107+
// Render regular authentication request details
108+
updateUI();
109+
}
110+
} else {
111+
Log.e(TAG, "Error requesting consent details", exception);
112+
throw new RuntimeException(exception);
113+
}
114+
}
115+
});
116+
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
117+
throw new RuntimeException(e);
118+
}
119+
} else {
120+
updateUI();
121+
}
83122
}
84123

85124
private void setupUI() {
86-
userText = (TextView) findViewById(R.id.userText);
87-
browserText = (TextView) findViewById(R.id.browserText);
88-
osText = (TextView) findViewById(R.id.osText);
89-
locationText = (TextView) findViewById(R.id.locationText);
90-
dateText = (TextView) findViewById(R.id.dateText);
91-
125+
// TODO: spinner fragment
92126
Button rejectButton = (Button) findViewById(R.id.rejectButton);
93127
assert rejectButton != null;
94128
rejectButton.setOnClickListener(new View.OnClickListener() {
@@ -109,17 +143,75 @@ public void onClick(View v) {
109143
}
110144

111145
private void updateUI() {
112-
userText.setText(enrollment.getUserId());
113-
browserText.setText(
146+
Fragment fragment;
147+
if (richConsent == null) {
148+
fragment = getStandardAuthenticationFragment();
149+
} else if (richConsent.getRequestedDetails().getAuthorizationDetails().isEmpty()) {
150+
fragment = getBasicConsentFragment();
151+
} else {
152+
List<PaymentInitiationDetails> paymentInitiationDetailsList = richConsent
153+
.getRequestedDetails()
154+
.filterAuthorizationDetailsByType(PaymentInitiationDetails.class);
155+
if (!paymentInitiationDetailsList.isEmpty()) {
156+
// For simplicity, in this example we render one single type
157+
fragment = getPaymentInitiationConsentFragment(paymentInitiationDetailsList.get(0));
158+
} else {
159+
fragment = getDynamicAuthorizationDetailsConsentFragment();
160+
}
161+
162+
}
163+
164+
getSupportFragmentManager().beginTransaction()
165+
.replace(R.id.authenticationDetailsFragmentContainer, fragment)
166+
.commit();
167+
168+
}
169+
170+
@NonNull
171+
private DynamicAuthorizationDetailsFragment getDynamicAuthorizationDetailsConsentFragment() {
172+
return DynamicAuthorizationDetailsFragment.newInstance(
173+
richConsent.getRequestedDetails().getBindingMessage(),
174+
notification.getDate().toString(),
175+
// For simplicity, in this example we render one single type
176+
richConsent.getRequestedDetails().getAuthorizationDetails().get(0)
177+
);
178+
}
179+
180+
@NonNull
181+
private ConsentPaymentInitiationFragment getPaymentInitiationConsentFragment(PaymentInitiationDetails paymentDetails) {
182+
return ConsentPaymentInitiationFragment.newInstance(
183+
richConsent.getRequestedDetails().getBindingMessage(),
184+
paymentDetails.getRemittanceInformation(),
185+
paymentDetails.getCreditorAccount().getAccountNumber(),
186+
paymentDetails.getInstructedAmount().getCurrency(),
187+
paymentDetails.getInstructedAmount().getAmount()
188+
);
189+
}
190+
191+
@NonNull
192+
private ConsentBasicDetailsFragment getBasicConsentFragment() {
193+
return ConsentBasicDetailsFragment.newInstance(
194+
richConsent.getRequestedDetails().getBindingMessage(),
195+
richConsent.getRequestedDetails().getScope(),
196+
notification.getDate().toString()
197+
);
198+
}
199+
200+
@NonNull
201+
private AuthenticationRequestDetailsFragment getStandardAuthenticationFragment() {
202+
return AuthenticationRequestDetailsFragment.newInstance(
203+
enrollment.getUserId(),
204+
114205
String.format("%s, %s",
115206
notification.getBrowserName(),
116-
notification.getBrowserVersion()));
117-
osText.setText(
207+
notification.getBrowserVersion()),
118208
String.format("%s, %s",
119209
notification.getOsName(),
120-
notification.getOsVersion()));
121-
locationText.setText(notification.getLocation());
122-
dateText.setText(notification.getDate().toString());
210+
notification.getOsVersion()),
211+
212+
notification.getLocation(),
213+
notification.getDate().toString()
214+
);
123215
}
124216

125217
private void rejectRequested() {

0 commit comments

Comments
 (0)