Skip to content

Commit

Permalink
Merge pull request #517 from /issues/506-user-info
Browse files Browse the repository at this point in the history
Support for /user/info endpoint
  • Loading branch information
hvge authored Feb 13, 2023
2 parents 630fb6c + f8ec6eb commit 3b659a5
Show file tree
Hide file tree
Showing 31 changed files with 1,408 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import io.getlime.security.powerauth.networking.response.IActivationRemoveListener;
import io.getlime.security.powerauth.networking.response.IActivationStatusListener;
import io.getlime.security.powerauth.networking.response.ICreateActivationListener;
import io.getlime.security.powerauth.networking.response.IUserInfoListener;
import io.getlime.security.powerauth.networking.response.UserInfo;
import io.getlime.security.powerauth.sdk.PowerAuthActivation;
import io.getlime.security.powerauth.sdk.PowerAuthSDK;
import io.getlime.security.powerauth.system.PowerAuthSystem;
Expand Down Expand Up @@ -417,4 +419,33 @@ public void onActivationCreateFailed(@NonNull Throwable t) {
assertTrue(powerAuthSDK.hasValidActivation());
}

// UserInfo

@Test
public void testUserInfo() throws Exception {
activationHelper.createStandardActivation(true, null);

final String userId = activationHelper.getUserId();
assertNotNull(activationHelper.getCreateActivationResult().getUserInfo());
assertNotNull(powerAuthSDK.getLastFetchedUserInfo());
assertEquals(userId, powerAuthSDK.getLastFetchedUserInfo().getSubject());
assertEquals(userId, activationHelper.getCreateActivationResult().getUserInfo().getSubject());

// Now fetch user info from the server
UserInfo info = AsyncHelper.await((AsyncHelper.Execution<UserInfo>) resultCatcher -> {
powerAuthSDK.fetchUserInfo(testHelper.getContext(), new IUserInfoListener() {
@Override
public void onUserInfoSucceed(@NonNull UserInfo userInfo) {
resultCatcher.completeWithResult(userInfo);
}

@Override
public void onUserInfoFailed(@NonNull Throwable t) {
resultCatcher.completeWithError(t);
}
});
});
assertEquals(userId, info.getSubject());
assertEquals(info, powerAuthSDK.getLastFetchedUserInfo());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2023 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getlime.security.powerauth.networking.response;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Map;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import static org.junit.Assert.*;

@RunWith(AndroidJUnit4.class)
public class UserInfoTests {

@Test
public void testEmptyObjectCreation() throws Exception {
UserInfo info = new UserInfo(null);
assertNull(info.getSubject());
assertNull(info.getName());
assertNull(info.getGivenName());
assertNull(info.getMiddleName());
assertNull(info.getFamilyName());
assertNull(info.getNickname());
assertNull(info.getPreferredUsername());
assertNull(info.getProfileUrl());
assertNull(info.getPictureUrl());
assertNull(info.getWebsiteUrl());
assertNull(info.getEmail());
assertFalse(info.isEmailVerified());
assertNull(info.getPhoneNumber());
assertFalse(info.isPhoneNumberVerified());
assertNull(info.getGender());
assertNull(info.getZoneInfo());
assertNull(info.getLocale());
UserAddress address = info.getAddress();
assertNull(address);
assertNull(info.getAllClaims().get("custom_claim"));
}

@Test
public void testStandardClaims() throws Exception {
final Date now = new Date(new Date().getTime()/1000*1000);
UserInfo info = deserializeFromJson("{" +
" \"sub\": \"123456\"," +
" \"name\": \"John Jacob Doe\"," +
" \"given_name\": \"John\"," +
" \"family_name\": \"Doe\"," +
" \"middle_name\": \"Jacob\"," +
" \"nickname\": \"jjd\"," +
" \"preferred_username\" : \"JacobTheGreat\"," +
" \"profile\": \"https://jjd.com/profile\"," +
" \"picture\": \"https://jjd.com/avatar.jpg\"," +
" \"website\": \"https://jjd.com\"," +
" \"email\": \"jacob@jjd.com\"," +
" \"email_verified\": true," +
" \"gender\": \"male\"," +
" \"birthdate\": \"1984-02-21\"," +
" \"zoneinfo\": \"Europe/Prague\"," +
" \"locale\": \"en-US\"," +
" \"phone_number\": \"+1 (425) 555-1212\"," +
" \"phone_number_verified\":true," +
" \"address\": {" +
" \"formatted\": \"Belehradska 858/23\\r\\n120 00 Prague - Vinohrady\\r\\nCzech Republic\"," +
" \"street_address\": \"Belehradska 858/23\\r\\nVinohrady\"," +
" \"locality\": \"Prague\"," +
" \"region\": \"Prague\"," +
" \"postal_code\": \"12000\"," +
" \"country\": \"Czech Republic\"" +
" }," +
" \"updated_at\": " + now.getTime()/1000 + "," +
" \"custom_claim\": \"Hello world!\"" +
"}");
assertNotNull(info);
assertEquals("123456", info.getSubject());
assertEquals("John Jacob Doe", info.getName());
assertEquals("John", info.getGivenName());
assertEquals("Jacob", info.getMiddleName());
assertEquals("Doe", info.getFamilyName());
assertEquals("jjd", info.getNickname());
assertEquals("JacobTheGreat", info.getPreferredUsername());
assertEquals("https://jjd.com/profile", info.getProfileUrl());
assertEquals("https://jjd.com/avatar.jpg", info.getPictureUrl());
assertEquals("https://jjd.com", info.getWebsiteUrl());
assertEquals("jacob@jjd.com", info.getEmail());
assertEquals(now, info.getUpdatedAt());
assertTrue(info.isEmailVerified());
assertEquals("+1 (425) 555-1212", info.getPhoneNumber());
assertTrue(info.isPhoneNumberVerified());
assertEquals("male",info.getGender());
assertEquals("Europe/Prague", info.getZoneInfo());
assertEquals("en-US", info.getLocale());
UserAddress address = info.getAddress();
assertNotNull(address);
assertEquals("Belehradska 858/23\n120 00 Prague - Vinohrady\nCzech Republic", address.getFormatted());
assertEquals("Belehradska 858/23\nVinohrady", address.getStreet());
assertEquals("Prague", address.getLocality());
assertEquals("Prague", address.getRegion());
assertEquals("12000", address.getPostalCode());
assertEquals("Czech Republic", address.getCountry());
assertEquals("Hello world!", info.getAllClaims().get("custom_claim"));

// Construct 1984-02-21
final Calendar calendar = new GregorianCalendar();
calendar.clear();
calendar.set(Calendar.YEAR, 1984);
calendar.set(Calendar.MONTH, 1);
calendar.set(Calendar.DAY_OF_MONTH, 21);
assertEquals(calendar.getTime(), info.getBirthdate());

info = deserializeFromJson("{" +
" \"email_verified\": false,\n" +
" \"phone_number_verified\":false\n" +
"}");
assertFalse(info.isPhoneNumberVerified());
assertFalse(info.isEmailVerified());
}

private UserInfo deserializeFromJson(String testData) {
if (gson == null) {
gson = new GsonBuilder().create();
}
final Map<String, Object> map = gson.fromJson(testData, new TypeToken<Map<String, Object>>(){}.getType());
return new UserInfo(map);
}

private Gson gson;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public EciesEncryptorFactory(@NonNull Session session, @Nullable byte[] possessi
*/
public @NonNull EciesEncryptor getEncryptor(@NonNull EciesEncryptorId identifier) throws PowerAuthErrorException {
if (identifier == EciesEncryptorId.NONE) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "'NONE' encryptor cannot be created.");
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "'NONE' encryptor cannot be created");
}
return getEncryptor(identifier.scope, identifier.sharedInfo1, identifier.hasMetadata);
}
Expand All @@ -77,23 +77,26 @@ public EciesEncryptorFactory(@NonNull Session session, @Nullable byte[] possessi
* @throws PowerAuthErrorException if factory doesn't have {@link #mPossessionUnlockKey} but is required,
* or if low level encryptor creation fails
*/
private @NonNull EciesEncryptor getEncryptor(@NonNull @EciesEncryptorScope int scope, @Nullable String sharedInfo1, boolean addMetaData) throws PowerAuthErrorException {
private @NonNull EciesEncryptor getEncryptor(@EciesEncryptorScope int scope, @Nullable String sharedInfo1, boolean addMetaData) throws PowerAuthErrorException {
final byte[] sharedInfo1Bytes = sharedInfo1 != null ? sharedInfo1.getBytes(Charset.defaultCharset()) : null;
final SignatureUnlockKeys unlockKeys;
final String activationId;
if (scope == EciesEncryptorScope.ACTIVATION) {
if (mPossessionUnlockKey == null) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "Device related key is missing for activation scoped encryptor.");
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "Device related key is missing for activation scoped encryptor");
}
activationId = mSession.getActivationIdentifier();
if (activationId == null) {
throw new PowerAuthErrorException(mSession.hasPendingActivation() ? PowerAuthErrorCodes.PENDING_ACTIVATION : PowerAuthErrorCodes.MISSING_ACTIVATION);
}
unlockKeys = new SignatureUnlockKeys(mPossessionUnlockKey, null, null);
} else {
activationId = null;
unlockKeys = null;
}
EciesEncryptor encryptor = mSession.getEciesEncryptor(scope, unlockKeys, sharedInfo1Bytes);
if (encryptor == null) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Failed to create ECIES encryptor.");
throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Failed to create ECIES encryptor");
}
if (addMetaData) {
encryptor.setMetadata(new EciesMetadata(mSession.getSessionSetup().applicationKey, activationId));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2023 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getlime.security.powerauth.networking.endpoints;

import com.google.gson.reflect.TypeToken;

import java.util.Map;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.getlime.security.powerauth.ecies.EciesEncryptorId;
import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition;

public class GetUserInfoEndpoint implements IEndpointDefinition<Map<String, Object>> {
@NonNull
@Override
public String getRelativePath() {
return "/pa/v3/user/info";
}

@NonNull
@Override
public String getHttpMethod() {
return "POST";
}

@Nullable
@Override
public String getAuthorizationUriId() {
return null;
}

@NonNull
@Override
public EciesEncryptorId getEncryptorId() {
return EciesEncryptorId.GENERIC_ACTIVATION_SCOPE;
}

@Nullable
@Override
public TypeToken<Map<String, Object>> getResponseType() {
return new TypeToken<Map<String, Object>>() {};
}

@Override
public boolean isSynchronized() {
return false;
}

@Override
public boolean isAvailableInProtocolUpgrade() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class ActivationLayer1Response {

private EciesEncryptedResponse activationData;
private Map<String, Object> customAttributes;
private Map<String, Object> userInfo;

/**
* Get encrypted activation data.
Expand Down Expand Up @@ -59,4 +60,20 @@ public Map<String, Object> getCustomAttributes() {
public void setCustomAttributes(Map<String, Object> customAttributes) {
this.customAttributes = customAttributes;
}

/**
* Get map containing information about user.
* @return Map containing information about user.
*/
public Map<String, Object> getUserInfo() {
return userInfo;
}

/**
* Set map containing information about user.
* @param userInfo ap containing information about user.
*/
public void setUserInfo(Map<String, Object> userInfo) {
this.userInfo = userInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ public class CreateActivationResult {
*/
private final @Nullable RecoveryData recoveryData;

/**
* Optional information about user. The value may be null in case that feature is not supported
* on the server.
*/
private final @Nullable UserInfo userInfo;

/**
* @param activationFingerprint Decimalized fingerprint calculated from data constructed
* from device's public key, server's public key and activation
Expand All @@ -57,11 +63,18 @@ public class CreateActivationResult {
* @param recoveryData {@link RecoveryData} object with information about activation
* recovery. The value may be null if feature is not supported,
* or configured on the server.
* @param userInfo {@link UserInfo} object with optional information
* about user.
*/
public CreateActivationResult(@NonNull String activationFingerprint, @Nullable Map<String, Object> customActivationAttributes, @Nullable RecoveryData recoveryData) {
public CreateActivationResult(
@NonNull String activationFingerprint,
@Nullable Map<String, Object> customActivationAttributes,
@Nullable RecoveryData recoveryData,
@Nullable UserInfo userInfo) {
this.activationFingerprint = activationFingerprint;
this.customActivationAttributes = customActivationAttributes;
this.recoveryData = recoveryData;
this.userInfo = userInfo;
}

/**
Expand Down Expand Up @@ -94,4 +107,12 @@ public CreateActivationResult(@NonNull String activationFingerprint, @Nullable M
public @Nullable RecoveryData getRecoveryData() {
return recoveryData;
}

/**
* @return {@link UserInfo} with optional information about user. The value is available
* only if the server provide such information about the user.
*/
public @Nullable UserInfo getUserInfo() {
return userInfo;
}
}
Loading

0 comments on commit 3b659a5

Please sign in to comment.