diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java index c773a24f..f89efd06 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java @@ -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; @@ -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) 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()); + } } diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/networking/response/UserInfoTests.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/networking/response/UserInfoTests.java new file mode 100644 index 00000000..64e8b94d --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/networking/response/UserInfoTests.java @@ -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 map = gson.fromJson(testData, new TypeToken>(){}.getType()); + return new UserInfo(map); + } + + private Gson gson; +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java index 9f85a16c..12d894c0 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java @@ -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); } @@ -77,15 +77,18 @@ 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; @@ -93,7 +96,7 @@ public EciesEncryptorFactory(@NonNull Session session, @Nullable byte[] possessi } 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)); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetUserInfoEndpoint.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetUserInfoEndpoint.java new file mode 100644 index 00000000..0feec2a4 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetUserInfoEndpoint.java @@ -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> { + @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> getResponseType() { + return new TypeToken>() {}; + } + + @Override + public boolean isSynchronized() { + return false; + } + + @Override + public boolean isAvailableInProtocolUpgrade() { + return false; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/ActivationLayer1Response.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/ActivationLayer1Response.java index 7eba1f2c..e2a089ad 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/ActivationLayer1Response.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/ActivationLayer1Response.java @@ -27,6 +27,7 @@ public class ActivationLayer1Response { private EciesEncryptedResponse activationData; private Map customAttributes; + private Map userInfo; /** * Get encrypted activation data. @@ -59,4 +60,20 @@ public Map getCustomAttributes() { public void setCustomAttributes(Map customAttributes) { this.customAttributes = customAttributes; } + + /** + * Get map containing information about user. + * @return Map containing information about user. + */ + public Map getUserInfo() { + return userInfo; + } + + /** + * Set map containing information about user. + * @param userInfo ap containing information about user. + */ + public void setUserInfo(Map userInfo) { + this.userInfo = userInfo; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/CreateActivationResult.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/CreateActivationResult.java index 26f5fe78..d9fbf4c8 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/CreateActivationResult.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/CreateActivationResult.java @@ -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 @@ -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 customActivationAttributes, @Nullable RecoveryData recoveryData) { + public CreateActivationResult( + @NonNull String activationFingerprint, + @Nullable Map customActivationAttributes, + @Nullable RecoveryData recoveryData, + @Nullable UserInfo userInfo) { this.activationFingerprint = activationFingerprint; this.customActivationAttributes = customActivationAttributes; this.recoveryData = recoveryData; + this.userInfo = userInfo; } /** @@ -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; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IUserInfoListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IUserInfoListener.java new file mode 100644 index 00000000..11630cdd --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IUserInfoListener.java @@ -0,0 +1,41 @@ +/* + * 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 androidx.annotation.MainThread; +import androidx.annotation.NonNull; + +/** + * Listener for user information retrieval. + */ +public interface IUserInfoListener { + /** + * Called when activation status retrieval succeeds. + * + * @param userInfo retrieved information about user. + */ + @MainThread + void onUserInfoSucceed(@NonNull UserInfo userInfo); + + /** + * Called when getting user information fails. + * + * @param t error that occurred during the user information retrieval. + */ + @MainThread + void onUserInfoFailed(@NonNull Throwable t); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/UserAddress.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/UserAddress.java new file mode 100644 index 00000000..8de86fff --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/UserAddress.java @@ -0,0 +1,89 @@ +/* + * 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 java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.sdk.impl.KVHelper; + +/** + * The `UserAddress` object contains address of end-user. + */ +public class UserAddress { + + private final KVHelper claims; + + /** + * Construct object with map with claims. + * @param claims Map with all claims representing information about the user. + */ + UserAddress(@Nullable Map claims) { + this.claims = new KVHelper<>(claims); + } + + /** + * @return Full collection of standard claims received from the server. + */ + public @NonNull Map getAllClaims() { + return claims.map; + } + + /** + * @return The full mailing address, with multiple lines if necessary. + */ + public @Nullable String getFormatted() { + return claims.valueAsMultilineString("formatted"); + } + + /** + * @return The street address component, which may include house number, street name, post office box, + * and other multi-line information. + */ + public @Nullable String getStreet() { + return claims.valueAsMultilineString("street_address"); + } + + /** + * @return City or locality component. + */ + public @Nullable String getLocality() { + return claims.valueAsString("locality"); + } + + /** + * @return State, province, prefecture or region component. + */ + public @Nullable String getRegion() { + return claims.valueAsString("region"); + } + + /** + * @return Zip code or postal code component. + */ + public @Nullable String getPostalCode() { + return claims.valueAsString("postal_code"); + } + + /** + * @return Country name component. + */ + public @Nullable String getCountry() { + return claims.valueAsString("country"); + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/UserInfo.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/UserInfo.java new file mode 100644 index 00000000..62018acc --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/UserInfo.java @@ -0,0 +1,198 @@ +/* + * 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 java.util.Date; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.sdk.impl.KVHelper; + +/** + * The `UserInfo` object contains additional information about the end-user. + */ +public class UserInfo { + + private final @NonNull KVHelper claims; + private final @Nullable Date birthdate; + private final @Nullable UserAddress address; + + /** + * Construct object with map with claims. + * @param claims Map with all claims representing information about the user. + */ + public UserInfo(@Nullable Map claims) { + this.claims = new KVHelper<>(claims); + this.birthdate = this.claims.valueAsDate("birthdate", "yyyy-MM-dd"); + final Map addressObject = this.claims.valueAsMap("address"); + this.address = addressObject != null ? new UserAddress(addressObject) : null; + } + + /** + * @return Full collection of standard claims received from the server. + */ + public @NonNull Map getAllClaims() { + return claims.map; + } + + /** + * @return The subject (end-user) identifier. This is a mandatory identifier by OpenID + * specification, but by default, not provided by PowerAuth based enrollment server. + */ + public @Nullable String getSubject() { + return claims.valueAsString("sub"); + } + + /** + * @return The full name of the end-user. + */ + public @Nullable String getName() { + return claims.valueAsString("name"); + } + + /** + * @return The given or first name of the end-user. + */ + public @Nullable String getGivenName() { + return claims.valueAsString("given_name"); + } + + /** + * @return The surname(s) or last name(s) of the end-user. + */ + public @Nullable String getFamilyName() { + return claims.valueAsString("family_name"); + } + + /** + * @return The middle name of the end-user. + */ + public @Nullable String getMiddleName() { + return claims.valueAsString("middle_name"); + } + + /** + * @return The casual name of the end-user. + */ + public @Nullable String getNickname() { + return claims.valueAsString("nickname"); + } + + /** + * @return The username by which the end-user wants to be referred to at the client application. + */ + public @Nullable String getPreferredUsername() { + return claims.valueAsString("preferred_username"); + } + + /** + * @return The URL of the profile page for the end-user. + */ + public @Nullable String getProfileUrl() { + return claims.valueAsString("profile"); + } + + /** + * @return The URL of the profile picture for the end-user. + */ + public @Nullable String getPictureUrl() { + return claims.valueAsString("picture"); + } + + /** + * @return The URL of the end-user's web page or blog. + */ + public @Nullable String getWebsiteUrl() { + return claims.valueAsString("website"); + } + + /** + * @return The end-user's preferred email address. + */ + public @Nullable String getEmail() { + return claims.valueAsString("email"); + } + + /** + * @return `true` if the end-user's email address has been verified, else `false`. + * Note that the value is false also when claim is not present in `claims` dictionary. + */ + public boolean isEmailVerified() { + return claims.valueAsBool("email_verified"); + } + + /** + * @return The end-user's preferred telephone number, typically in E.164 format, for example + * `+1 (425) 555-1212` or `+56 (2) 687 2400`. + */ + public @Nullable String getPhoneNumber() { + return claims.valueAsString("phone_number"); + } + + /** + * @return `true` if the end-user's telephone number has been verified, else `false`. Note that + * value is false also when claim is not present in `claims` dictionary. + */ + public boolean isPhoneNumberVerified() { + return claims.valueAsBool("phone_number_verified"); + } + + /** + * @return The end-user's gender. + */ + public @Nullable String getGender() { + return claims.valueAsString("gender"); + } + + /** + * @return The end-user's birthday. + */ + public @Nullable Date getBirthdate() { + return birthdate; + } + + /** + * @return The end-user's time zone, e.g. `Europe/Paris` or `America/Los_Angeles`. + */ + public @Nullable String getZoneInfo() { + return claims.valueAsString("zoneinfo"); + } + + /** + * @return The end-user's locale, represented as a BCP47 language tag. This is typically + * an ISO 639-1 Alpha-2 language code in lowercase and an ISO 3166-1 Alpha-2 country code + * in uppercase, separated by a dash. For example, `en-US` or `fr-CA`. + */ + public @Nullable String getLocale() { + return claims.valueAsString("locale"); + } + + /** + * @return An object describing the end-user's preferred postal address. + */ + public @Nullable UserAddress getAddress() { + return address; + } + + /** + * @return Time the end-user's information was last updated. + */ + public @Nullable Date getUpdatedAt() { + return claims.valueAsTimestamp("updated_at"); + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java index da243e44..c2813306 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java @@ -70,6 +70,7 @@ import io.getlime.security.powerauth.networking.client.JsonSerialization; import io.getlime.security.powerauth.networking.endpoints.ConfirmRecoveryCodeEndpoint; import io.getlime.security.powerauth.networking.endpoints.CreateActivationEndpoint; +import io.getlime.security.powerauth.networking.endpoints.GetUserInfoEndpoint; import io.getlime.security.powerauth.networking.endpoints.RemoveActivationEndpoint; import io.getlime.security.powerauth.networking.endpoints.ValidateSignatureEndpoint; import io.getlime.security.powerauth.networking.endpoints.VaultUnlockEndpoint; @@ -95,7 +96,9 @@ import io.getlime.security.powerauth.networking.response.IDataSignatureListener; import io.getlime.security.powerauth.networking.response.IFetchEncryptionKeyListener; import io.getlime.security.powerauth.networking.response.IGetRecoveryDataListener; +import io.getlime.security.powerauth.networking.response.IUserInfoListener; import io.getlime.security.powerauth.networking.response.IValidatePasswordListener; +import io.getlime.security.powerauth.networking.response.UserInfo; import io.getlime.security.powerauth.sdk.impl.CompositeCancelableTask; import io.getlime.security.powerauth.sdk.impl.DefaultExecutorProvider; import io.getlime.security.powerauth.sdk.impl.DefaultPossessionFactorEncryptionKeyProvider; @@ -745,7 +748,9 @@ public void onNetworkResponse(@NonNull ActivationLayer1Response response) { final ActivationStep2Result step2Result = mSession.validateActivationResponse(step2Param); // if (step2Result.errorCode == ErrorCode.OK) { - final CreateActivationResult result = new CreateActivationResult(step2Result.activationFingerprint, response.getCustomAttributes(), recoveryData); + final UserInfo userInfo = response.getUserInfo() != null ? new UserInfo(response.getUserInfo()) : null; + final CreateActivationResult result = new CreateActivationResult(step2Result.activationFingerprint, response.getCustomAttributes(), recoveryData, userInfo); + setLastFetchedUserInfo(userInfo); listener.onActivationCreateSucceed(result); return; } @@ -1165,6 +1170,82 @@ public int commitActivationWithAuthentication(@NonNull Context context, @NonNull } } + // + // User Info + // + + /** + * Variable keeping last fetched information about user. + */ + private UserInfo mLastFetchedUserInfo = null; + + /** + * Return last fetched information about the user. The information about user is optional and + * must be supported by the server. The value is updated during the activation process or by + * calling {@link #fetchUserInfo(Context, IUserInfoListener)}. + * + * @return {@link UserInfo} object or {@code null} if information is not retrieved yet. + */ + public @Nullable UserInfo getLastFetchedUserInfo() { + try { + mLock.lock(); + return mLastFetchedUserInfo; + } finally { + mLock.unlock(); + } + } + + /** + * Store retrieved information about the user. + * @param userInfo New instance of {@link UserInfo} object to keep. + */ + private void setLastFetchedUserInfo(@Nullable UserInfo userInfo) { + try { + mLock.lock(); + mLastFetchedUserInfo = userInfo; + } finally { + mLock.unlock(); + } + } + + /** + * Fetch information about the user from the server. If operation succeed, then the user + * information object is also internally stored and available in {@link #getLastFetchedUserInfo()} + * method. + * + * @param context Android context. + * @param listener A callback called once the user info is retrieved from the server. + * @return {@link ICancelable} object associated with the pending HTTP request. + * @throws PowerAuthMissingConfigException thrown in case configuration is not present. + */ + @Nullable + public ICancelable fetchUserInfo(@NonNull Context context, @NonNull IUserInfoListener listener) { + // State validations + checkForValidSetup(); + // Execute HTTP request. + return mClient.post( + null, + new GetUserInfoEndpoint(), + getCryptoHelper(context), + new INetworkResponseListener>() { + @Override + public void onNetworkResponse(@NonNull Map response) { + final UserInfo userInfo = new UserInfo(response); + setLastFetchedUserInfo(userInfo); + listener.onUserInfoSucceed(userInfo); + } + + @Override + public void onNetworkError(@NonNull Throwable throwable) { + listener.onUserInfoFailed(throwable); + } + + @Override + public void onCancel() { + } + }); + } + // // Activation Status // @@ -1404,6 +1485,7 @@ private void clearCachedData() { try { mLock.lock(); mLastFetchedActivationStatus = null; + mLastFetchedUserInfo = null; } finally { mLock.unlock(); } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java new file mode 100644 index 00000000..1fcc24a1 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/KVHelper.java @@ -0,0 +1,91 @@ +/* + * 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.sdk.impl; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * The `KVHelper` class provides a type-safe access to values stored in the {@code Map}. + * The object is typically useful while processing data deserialized from JSON. + * + * @param Type of key in the map. + */ +public class KVHelper { + + public @NonNull Map map; + + public KVHelper(Map map) { + this.map = map != null ? map : Collections.emptyMap(); + } + + @Nullable + public String valueAsString(@NonNull K key) { + final Object v = map.get(key); + return v instanceof String ? (String) v : null; + } + + @Nullable + public String valueAsMultilineString(@NonNull K key) { + final String s = valueAsString(key); + if (s != null) { + return s.replace("\r\n", "\n"); + } + return s; + } + + public boolean valueAsBool(@NonNull K key) { + final Object v = map.get(key); + return v instanceof Boolean ? (Boolean) v : false; + } + + @SuppressWarnings("unchecked") + @Nullable + public Map valueAsMap(@NonNull K key) { + final Object v = map.get(key); + return v instanceof Map ? (Map) v : null; + } + + @Nullable + public Date valueAsTimestamp(@NonNull K key) { + final Object v = map.get(key); + if (v instanceof Number) { + return new Date(1000 * ((Number) v).longValue()); + } + return null; + } + + @Nullable + public Date valueAsDate(@NonNull K key, @NonNull String format) { + final String v = valueAsString(key); + if (v != null) { + try { + return new SimpleDateFormat(format, Locale.US).parse(v); + } catch (ParseException e) { + return null; + } + } + return null; + } +} diff --git a/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj b/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj index b6f7a16f..604c68fe 100644 --- a/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj +++ b/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj @@ -83,6 +83,14 @@ BF28AA2D27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = BF28AA2A27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF28AA2E27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28AA2B27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.m */; }; BF28AA2F27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28AA2B27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.m */; }; + BF28C1A829815D6900E2CD8E /* PowerAuthUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = BF28C1A629815D6900E2CD8E /* PowerAuthUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BF28C1A929815D6900E2CD8E /* PowerAuthUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = BF28C1A629815D6900E2CD8E /* PowerAuthUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BF28C1AA29815D6900E2CD8E /* PowerAuthUserInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28C1A729815D6900E2CD8E /* PowerAuthUserInfo.m */; }; + BF28C1AB29815D6900E2CD8E /* PowerAuthUserInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28C1A729815D6900E2CD8E /* PowerAuthUserInfo.m */; }; + BF28C1B3298168C100E2CD8E /* PowerAuthUserInfo+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = BF28C1B2298168C100E2CD8E /* PowerAuthUserInfo+Private.h */; }; + BF28C1B4298168C100E2CD8E /* PowerAuthUserInfo+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = BF28C1B2298168C100E2CD8E /* PowerAuthUserInfo+Private.h */; }; + BF28C1C42982A05100E2CD8E /* PowerAuthUserInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28C1C32982A05100E2CD8E /* PowerAuthUserInfoTests.m */; }; + BF28C1C52982A05100E2CD8E /* PowerAuthUserInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28C1C32982A05100E2CD8E /* PowerAuthUserInfoTests.m */; }; BF30DB5C206CEAE900430C12 /* PA2VaultUnlockRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = BF30DB5A206CEAE900430C12 /* PA2VaultUnlockRequest.h */; }; BF30DB5D206CEAE900430C12 /* PA2VaultUnlockRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = BF30DB5B206CEAE900430C12 /* PA2VaultUnlockRequest.m */; }; BF31607527E895DD00EDA287 /* PA2SharedLock.h in Headers */ = {isa = PBXBuildFile; fileRef = BF31607327E895DD00EDA287 /* PA2SharedLock.h */; }; @@ -660,6 +668,10 @@ BF28AA2527EC710F00FBFCEB /* PowerAuthSharingConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PowerAuthSharingConfiguration.m; sourceTree = ""; }; BF28AA2A27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerAuthExternalPendingOperation.h; sourceTree = ""; }; BF28AA2B27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PowerAuthExternalPendingOperation.m; sourceTree = ""; }; + BF28C1A629815D6900E2CD8E /* PowerAuthUserInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerAuthUserInfo.h; sourceTree = ""; }; + BF28C1A729815D6900E2CD8E /* PowerAuthUserInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PowerAuthUserInfo.m; sourceTree = ""; }; + BF28C1B2298168C100E2CD8E /* PowerAuthUserInfo+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PowerAuthUserInfo+Private.h"; sourceTree = ""; }; + BF28C1C32982A05100E2CD8E /* PowerAuthUserInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PowerAuthUserInfoTests.m; sourceTree = ""; }; BF3017681FE0252E0099B253 /* PA2PrivateRemoteTokenProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2PrivateRemoteTokenProvider.h; sourceTree = ""; }; BF30176A1FE026F80099B253 /* PA2PrivateHttpTokenProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2PrivateHttpTokenProvider.h; sourceTree = ""; }; BF30176B1FE026F80099B253 /* PA2PrivateHttpTokenProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2PrivateHttpTokenProvider.m; sourceTree = ""; }; @@ -1005,6 +1017,8 @@ BF0825182630514D00B34E24 /* PowerAuthErrorConstants.m */, BFFECEB326383CB9001DA7A9 /* PowerAuthDeprecated.h */, BFFECEC226383E48001DA7A9 /* PowerAuthDeprecated.m */, + BF28C1A629815D6900E2CD8E /* PowerAuthUserInfo.h */, + BF28C1A729815D6900E2CD8E /* PowerAuthUserInfo.m */, ); name = sdk; sourceTree = ""; @@ -1081,6 +1095,7 @@ BF08257C2632E0E400B34E24 /* PowerAuthActivationStatus+Private.h */, BF4B357E2851FDA8009A03EF /* PowerAuthAuthentication+Private.h */, BF6B39EF27F4777D00BDF579 /* PowerAuthExternalPendingOperation+Private.h */, + BF28C1B2298168C100E2CD8E /* PowerAuthUserInfo+Private.h */, BF585FD2215D328B00DE49C3 /* PA2PrivateCryptoHelper.h */, BF2007522152B44F001F3614 /* PA2PrivateEncryptorFactory.h */, BF2007532152B44F001F3614 /* PA2PrivateEncryptorFactory.m */, @@ -1232,6 +1247,7 @@ BF667EC52835A0B2006E97F8 /* PowerAuthCustomHeaderRequestInterceptorTests.m */, BF667EC62835A0B2006E97F8 /* PowerAuthBasicHttpAuthenticationRequestInterceptorTests.m */, BF215F01287D8C9300EC3F3E /* PowerAuthAuthenticationTests.m */, + BF28C1C32982A05100E2CD8E /* PowerAuthUserInfoTests.m */, BF667EC72835A0B2006E97F8 /* Info.plist */, ); path = PowerAuth2Tests; @@ -1434,11 +1450,13 @@ BF5EB51124C85FE200F9DDB2 /* PowerAuthKeychain.h in Headers */, BF5EB51224C85FE200F9DDB2 /* PowerAuthConfiguration.h in Headers */, BFFECF2A26385BC9001DA7A9 /* PowerAuthActivationCode+Private.h in Headers */, + BF28C1A929815D6900E2CD8E /* PowerAuthUserInfo.h in Headers */, BF6B39EC27F312D100BDF579 /* PA2SharedMemory.h in Headers */, BF5EB51324C85FE200F9DDB2 /* PowerAuthLog.h in Headers */, BFC192AB27FC95A1001455C1 /* PA2TokenDataLock.h in Headers */, BF4B35802851FDA8009A03EF /* PowerAuthAuthentication+Private.h in Headers */, BF5EB51724C85FE200F9DDB2 /* PowerAuthAuthentication.h in Headers */, + BF28C1B4298168C100E2CD8E /* PowerAuthUserInfo+Private.h in Headers */, BF5EB51A24C85FE200F9DDB2 /* PA2GetTokenResponse.h in Headers */, BF5EB51B24C85FE200F9DDB2 /* PA2RemoveTokenRequest.h in Headers */, BF5EB51D24C85FE200F9DDB2 /* PowerAuthCustomHeaderRequestInterceptor.h in Headers */, @@ -1550,6 +1568,7 @@ BF2007542152B44F001F3614 /* PA2PrivateEncryptorFactory.h in Headers */, BF8CF4FE2032EB41002A6B6E /* PowerAuthAuthorizationHttpHeader.h in Headers */, BF8CF5002032EB41002A6B6E /* PA2WCSessionDataHandler.h in Headers */, + BF28C1B3298168C100E2CD8E /* PowerAuthUserInfo+Private.h in Headers */, BF8CF5012032EB41002A6B6E /* PowerAuthKeychainConfiguration.h in Headers */, BF1ED0B72844D3D200D6B380 /* PA2GroupedTask.h in Headers */, BFF6716B243376B0009279CA /* PowerAuthActivation.h in Headers */, @@ -1557,6 +1576,7 @@ BF6B39EB27F312D100BDF579 /* PA2SharedMemory.h in Headers */, BF8CF5032032EB41002A6B6E /* PowerAuthWCSessionManager+Private.h in Headers */, BF1EC6CF223BD3BB00883236 /* PA2CreateActivationRecoveryData.h in Headers */, + BF28C1A829815D6900E2CD8E /* PowerAuthUserInfo.h in Headers */, BF1ED06E283CEB2700D6B380 /* PowerAuthKeychainAuthentication.h in Headers */, BFC192A127FC8C3F001455C1 /* PA2SessionInterface.h in Headers */, BF5344672163D104003C201C /* PA2Error+Decodable.h in Headers */, @@ -1937,6 +1957,7 @@ files = ( BF667ED52835A13F006E97F8 /* AsyncHelper.m in Sources */, BF369FCA285370DD0004C454 /* PA2GroupedTaskTests.m in Sources */, + BF28C1C42982A05100E2CD8E /* PowerAuthUserInfoTests.m in Sources */, BF667ED02835A0B2006E97F8 /* PowerAuthBasicHttpAuthenticationRequestInterceptorTests.m in Sources */, BF215F02287D8C9300EC3F3E /* PowerAuthAuthenticationTests.m in Sources */, BF97A72A28A3E594002F3ACE /* PowerAuthCorePasswordHelper.m in Sources */, @@ -1973,6 +1994,7 @@ BF5EB4B224C85FE200F9DDB2 /* PowerAuthSDK.m in Sources */, BF5EB4B324C85FE200F9DDB2 /* PowerAuthRestApiError.m in Sources */, BF5EB4B424C85FE200F9DDB2 /* PA2ObjectSerialization.m in Sources */, + BF28C1AB29815D6900E2CD8E /* PowerAuthUserInfo.m in Sources */, BF5EB4B524C85FE200F9DDB2 /* PA2CreateActivationRequestData.m in Sources */, BF08254D2631607C00B34E24 /* PowerAuthActivationStatus.m in Sources */, BF1ED071283CEB2700D6B380 /* PowerAuthKeychainAuthentication.m in Sources */, @@ -2029,6 +2051,7 @@ files = ( BF667ED42835A13E006E97F8 /* AsyncHelper.m in Sources */, BF369FCB285370DD0004C454 /* PA2GroupedTaskTests.m in Sources */, + BF28C1C52982A05100E2CD8E /* PowerAuthUserInfoTests.m in Sources */, BF667ED12835A0B2006E97F8 /* PowerAuthBasicHttpAuthenticationRequestInterceptorTests.m in Sources */, BF215F03287D8C9300EC3F3E /* PowerAuthAuthenticationTests.m in Sources */, BF97A72B28A3E594002F3ACE /* PowerAuthCorePasswordHelper.m in Sources */, @@ -2177,6 +2200,7 @@ BFDFED9920BEEFAC0094138A /* PowerAuthLog.m in Sources */, BF8CF4C32032EB41002A6B6E /* PowerAuthWCSessionManager_IOSServices.m in Sources */, BFF6716C243376B0009279CA /* PowerAuthActivation.m in Sources */, + BF28C1AA29815D6900E2CD8E /* PowerAuthUserInfo.m in Sources */, BF08251B2630514D00B34E24 /* PowerAuthErrorConstants.m in Sources */, BF28AA1927EB1EDE00FBFCEB /* PA2SessionDataProvider.m in Sources */, BF1EC6D0223BD3BB00883236 /* PA2CreateActivationRecoveryData.m in Sources */, diff --git a/proj-xcode/PowerAuth2/PowerAuthActivationResult.h b/proj-xcode/PowerAuth2/PowerAuthActivationResult.h index c046da79..4cca0418 100644 --- a/proj-xcode/PowerAuth2/PowerAuthActivationResult.h +++ b/proj-xcode/PowerAuth2/PowerAuthActivationResult.h @@ -15,6 +15,7 @@ */ #import +#import /** The PowerAuthActivationResult object represents successfull result from the activation @@ -36,5 +37,10 @@ are no custom attributes available. */ @property (nonatomic, strong, nullable) NSDictionary* customAttributes; +/** + Information about user's identity. The value is optional and depending on whether the server + implementation provide such information in the time of activation. + */ +@property (nonatomic, strong, nullable) PowerAuthUserInfo * userInfo; @end diff --git a/proj-xcode/PowerAuth2/PowerAuthSDK.h b/proj-xcode/PowerAuth2/PowerAuthSDK.h index 9d3741f0..32f401a7 100644 --- a/proj-xcode/PowerAuth2/PowerAuthSDK.h +++ b/proj-xcode/PowerAuth2/PowerAuthSDK.h @@ -28,6 +28,7 @@ #import #import #import +#import // Deprecated #import @@ -788,3 +789,23 @@ - (BOOL) removeExternalEncryptionKey:(NSError * _Nullable * _Nullable)error; @end + +#pragma mark - User Info + +@interface PowerAuthSDK (UserInfo) + +/** + Contains last properly fetched instance of `PowerAuthUserInfo` object. The value is updated during the activation process + or by calling `fetchUserInfo()` function. + */ +@property (nonatomic, readonly, nullable) PowerAuthUserInfo * lastFetchedUserInfo; + +/** + Fetch information about the user from the server. If operation succeed, then the user information object is also stored to `lastFetchedUserInfo` property. + @param callback The callback method with an user info data. + @return PowerAuthOperationTask associated with the running request. + */ +- (nullable id) fetchUserInfo:(nonnull void(^)(PowerAuthUserInfo * _Nullable userInfo, NSError * _Nullable error))callback + NS_SWIFT_NAME(fetchUserInfo(callback:)); + +@end diff --git a/proj-xcode/PowerAuth2/PowerAuthSDK.m b/proj-xcode/PowerAuth2/PowerAuthSDK.m index 40879ff3..9b008cfb 100644 --- a/proj-xcode/PowerAuth2/PowerAuthSDK.m +++ b/proj-xcode/PowerAuth2/PowerAuthSDK.m @@ -70,6 +70,8 @@ @implementation PowerAuthSDK /// Current pending status task. PA2GetActivationStatusTask * _getStatusTask; PowerAuthActivationStatus * _lastFetchedActivationStatus; + /// User info + PowerAuthUserInfo * _lastFetchedUserInfo; } #pragma mark - Private methods @@ -775,12 +777,13 @@ - (NSString*) activationFingerprint requestData.devicePublicKey = resultStep1.devicePublicKey; // Now we need to ecrypt request data with the Layer2 encryptor. - PowerAuthCoreEciesEncryptor * privateEncryptor = [self encryptorWithId:PA2EncryptorId_ActivationPayload]; - - // Encrypt payload and put it directly to the request object. - request.activationData = [PA2ObjectSerialization encryptObject:requestData - encryptor:privateEncryptor - error:&localError]; + PowerAuthCoreEciesEncryptor * privateEncryptor = [self encryptorWithId:PA2EncryptorId_ActivationPayload error:&localError]; + if (!localError) { + // Encrypt payload and put it directly to the request object. + request.activationData = [PA2ObjectSerialization encryptObject:requestData + encryptor:privateEncryptor + error:&localError]; + } if (!localError) { // Everything looks OS, so finally, try notify other apps that this instance started the activation. localError = [_sessionInterface startExternalPendingOperation:PowerAuthExternalPendingOperationType_Activation]; @@ -835,6 +838,8 @@ - (NSString*) activationFingerprint result.activationFingerprint = resultStep2.activationFingerprint; result.customAttributes = response.customAttributes; result.activationRecovery = activationRecoveryData; + result.userInfo = [[PowerAuthUserInfo alloc] initWithDictionary:response.userInfo]; + [self setLastFetchedUserInfo:result.userInfo]; return [PA2Result success:result]; } else { localError = PA2MakeError(PowerAuthErrorCode_InvalidActivationData, @"Failed to verify response from the server"); @@ -971,6 +976,7 @@ - (void) clearCachedData { [_lock lock]; _lastFetchedActivationStatus = nil; + _lastFetchedUserInfo = nil; [_lock unlock]; } @@ -1480,7 +1486,7 @@ @implementation PowerAuthSDK (E2EE) - (PowerAuthCoreEciesEncryptor*) eciesEncryptorForApplicationScope { PA2PrivateEncryptorFactory * factory = [[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:_sessionInterface deviceRelatedKey:nil]; - return [factory encryptorWithId:PA2EncryptorId_GenericApplicationScope]; + return [factory encryptorWithId:PA2EncryptorId_GenericApplicationScope error:nil]; } - (PowerAuthCoreEciesEncryptor*) eciesEncryptorForActivationScope @@ -1492,7 +1498,7 @@ - (PowerAuthCoreEciesEncryptor*) eciesEncryptorForActivationScope } NSData * deviceKey = [self deviceRelatedKey]; PA2PrivateEncryptorFactory * factory = [[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:_sessionInterface deviceRelatedKey:deviceKey]; - return [factory encryptorWithId:PA2EncryptorId_GenericActivationScope]; + return [factory encryptorWithId:PA2EncryptorId_GenericActivationScope error:nil]; }]; } @@ -1701,3 +1707,43 @@ - (BOOL) removeExternalEncryptionKey:(NSError **)error } @end + +#pragma mark - User Info + +@implementation PowerAuthSDK (UserInfo) + +- (PowerAuthUserInfo*) lastFetchedUserInfo +{ + [_lock lock]; + PowerAuthUserInfo * info = _lastFetchedUserInfo; + [_lock unlock]; + return info; +} + +- (void) setLastFetchedUserInfo:(PowerAuthUserInfo*)lastFetchedUserInfo +{ + [_lock lock]; + _lastFetchedUserInfo = lastFetchedUserInfo; + [_lock unlock]; +} + +- (id) fetchUserInfo:(void (^)(PowerAuthUserInfo *, NSError *))callback +{ + [self checkForValidSetup]; + + // Post request + return [_client postObject:nil + to:[PA2RestApiEndpoint getUserInfo] + completion:^(PowerAuthRestApiResponseStatus status, id response, NSError *error) { + PowerAuthUserInfo * result; + if (status == PowerAuthRestApiResponseStatus_OK) { + result = (PowerAuthUserInfo*)response; + [self setLastFetchedUserInfo:result]; + } else { + result = nil; + } + callback(result, error); + }]; +} + +@end diff --git a/proj-xcode/PowerAuth2/PowerAuthUserInfo.h b/proj-xcode/PowerAuth2/PowerAuthUserInfo.h new file mode 100644 index 00000000..d67b5078 --- /dev/null +++ b/proj-xcode/PowerAuth2/PowerAuthUserInfo.h @@ -0,0 +1,95 @@ +/* + * 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. + */ + +#import + +@class PowerAuthUserAddress; + +/// The `PowerAuthUserInfo` object contains additional information about the end-user. +@interface PowerAuthUserInfo : NSObject + +/// The subject (end-user) identifier. +@property (nonatomic, readonly, nullable) NSString * subject; +/// The full name of the end-user. +@property (nonatomic, readonly, nullable) NSString * name; +/// The given or first name of the end-user. +@property (nonatomic, readonly, nullable) NSString * givenName; +/// The surname(s) or last name(s) of the end-user. +@property (nonatomic, readonly, nullable) NSString * familyName; +/// The middle name of the end-user. +@property (nonatomic, readonly, nullable) NSString * middleName; +/// The casual name of the end-user. +@property (nonatomic, readonly, nullable) NSString * nickname; +/// The username by which the end-user wants to be referred to at the client application. +@property (nonatomic, readonly, nullable) NSString * preferredUsername; +/// The URL of the profile page for the end-user. +@property (nonatomic, readonly, nullable) NSString * profileUrl; +/// The URL of the profile picture for the end-user. +@property (nonatomic, readonly, nullable) NSString * pictureUrl; +/// The URL of the end-user's web page or blog. +@property (nonatomic, readonly, nullable) NSString * websiteUrl; +/// The end-user's preferred email address. +@property (nonatomic, readonly, nullable) NSString * email; +/// True if the end-user's email address has been verified, else false. +/// Note that value is false also when claim is not present in `claims` dictionary. +@property (nonatomic, readonly) BOOL isEmailVerified; +/// The end-user's preferred telephone number, typically in E.164 format, for example `+1 (425) 555-1212` +/// or `+56 (2) 687 2400`. +@property (nonatomic, readonly, nullable) NSString * phoneNumber; +/// True if the end-user's telephone number has been verified, else false. +/// Note that value is false also when claim is not present in `claims` dictionary. +@property (nonatomic, readonly) BOOL isPhoneNumberVerified; +/// The end-user's gender. +@property (nonatomic, readonly, nullable) NSString * gender; +/// The end-user's birthday. +@property (nonatomic, readonly, nullable) NSDate * birthdate; +/// The end-user's time zone, e.g. `Europe/Paris` or `America/Los_Angeles`. +@property (nonatomic, readonly, nullable) NSString * zoneInfo; +/// The end-user's locale, represented as a BCP47 language tag. This is typically an ISO 639-1 Alpha-2 +/// language code in lowercase and an ISO 3166-1 Alpha-2 country code in uppercase, separated by a dash. +/// For example, `en-US` or `fr-CA`. +@property (nonatomic, readonly, nullable) NSString * locale; +/// An object describing the end-user's preferred postal address. +@property (nonatomic, readonly, nullable) PowerAuthUserAddress * address; +/// Time the end-user's information was last updated. +@property (nonatomic, readonly, nullable) NSDate * updatedAt; + +/// Contains full collection of standard claims received from the server. +@property (nonatomic, readonly, nonnull) NSDictionary* allClaims; + +@end + +/// The `PowerAuthUserAddress` object contains address of end-user. +@interface PowerAuthUserAddress : NSObject + +/// The full mailing address, with multiple lines if necessary. +@property (nonatomic, readonly, nullable) NSString * formatted; +/// The street address component, which may include house number, street name, post office box, +/// and other multi-line information. +@property (nonatomic, readonly, nullable) NSString * street; +/// City or locality component. +@property (nonatomic, readonly, nullable) NSString * locality; +/// State, province, prefecture or region component. +@property (nonatomic, readonly, nullable) NSString * region; +/// Zip code or postal code component. +@property (nonatomic, readonly, nullable) NSString * postalCode; +/// Country name component. +@property (nonatomic, readonly, nullable) NSString * country; + +/// Contains full collection of standard claims received from the server. +@property (nonatomic, readonly, nonnull) NSDictionary* allClaims; + +@end diff --git a/proj-xcode/PowerAuth2/PowerAuthUserInfo.m b/proj-xcode/PowerAuth2/PowerAuthUserInfo.m new file mode 100644 index 00000000..640a57ee --- /dev/null +++ b/proj-xcode/PowerAuth2/PowerAuthUserInfo.m @@ -0,0 +1,136 @@ +/* + * 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. + */ + +#import "PowerAuthUserInfo+Private.h" +#import "PA2PrivateMacros.h" +#import "PowerAuthLog.h" + +@implementation PowerAuthUserInfo + +#pragma mark - Support functions + +// Simple string getter +#define CLAIMS_GETTER(propertyName, claimName) \ + - (NSString *) propertyName { return PA2ObjectAs(_allClaims[claimName], NSString); } + +// Getter for multi-line string, patching MS-DOS newlines. +inline static NSString * _GetterMLString(id object) +{ + NSString * s = PA2ObjectAs(object, NSString); + return [s stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"]; +} +#define CLAIMS_GETTER_ML(propertyName, claimName) \ + - (NSString *) propertyName { return _GetterMLString(_allClaims[claimName]); } + +// Simple BOOL getter +#define CLAIMS_GETTER_BOOL(propertyName, claimName) \ + - (BOOL) propertyName { return [PA2ObjectAs(_allClaims[claimName], NSNumber) boolValue]; } + +// Getter for returning timestamp +inline static NSDate * _GetterTimestamp(id object) +{ + NSNumber * n = PA2ObjectAs(object, NSNumber); + return n ? [NSDate dateWithTimeIntervalSince1970:[n doubleValue]] : nil; +} +#define CLAIMS_GETTER_TIMESTAMP(propertyName, claimName) \ + - (NSDate *) propertyName { return _GetterTimestamp(_allClaims[claimName]); } + + +#pragma mark - Initialization + +- (instancetype) initWithDictionary:(NSDictionary*)dictionary +{ + if (!dictionary) { + return nil; + } + self = [super init]; + if (self) { + // Keep the whole dictionary. + _allClaims = dictionary; + // "birthdate" + NSString * birthDate = PA2ObjectAs(dictionary[@"birthdate"], NSString); + if (birthDate) { + NSDateFormatter * formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd"; + _birthdate = [formatter dateFromString:birthDate]; + } + // "address" + NSDictionary * address = PA2ObjectAs(dictionary[@"address"], NSDictionary); + _address = [[PowerAuthUserAddress alloc] initWithDictionary:address]; + } + return self; +} + +#pragma mark - Getters + +CLAIMS_GETTER(subject, @"sub") +CLAIMS_GETTER(name, @"name") +CLAIMS_GETTER(givenName, @"given_name") +CLAIMS_GETTER(familyName, @"family_name") +CLAIMS_GETTER(middleName, @"middle_name") +CLAIMS_GETTER(nickname, @"nickname") +CLAIMS_GETTER(preferredUsername, @"preferred_username") +CLAIMS_GETTER(profileUrl, @"profile") +CLAIMS_GETTER(pictureUrl, @"picture") +CLAIMS_GETTER(websiteUrl, @"website") +CLAIMS_GETTER(email, @"email") +CLAIMS_GETTER(phoneNumber, @"phone_number") +CLAIMS_GETTER_BOOL(isEmailVerified, @"email_verified") +CLAIMS_GETTER_BOOL(isPhoneNumberVerified, @"phone_number_verified") +CLAIMS_GETTER(gender, @"gender") +CLAIMS_GETTER(zoneInfo, @"zoneinfo") +CLAIMS_GETTER(locale, @"locale") +CLAIMS_GETTER_TIMESTAMP(updatedAt, @"updated_at") + +#ifdef DEBUG +- (NSString*) description +{ + return [NSString stringWithFormat:@"", _allClaims]; +} +#endif // DEBUG + +@end + + +@implementation PowerAuthUserAddress + +- (instancetype) initWithDictionary:(NSDictionary*)dictionary +{ + if (!dictionary) { + return nil; + } + self = [super init]; + if (self) { + _allClaims = dictionary; + } + return self; +} + +CLAIMS_GETTER_ML(formatted, @"formatted") +CLAIMS_GETTER_ML(street, @"street_address") +CLAIMS_GETTER(locality, @"locality") +CLAIMS_GETTER(region, @"region") +CLAIMS_GETTER(postalCode, @"postal_code") +CLAIMS_GETTER(country, @"country") + +#ifdef DEBUG +- (NSString*) description +{ + return [NSString stringWithFormat:@"", _allClaims]; +} +#endif // DEBUG + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.h b/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.h index 9f8d54d4..86f71ed2 100644 --- a/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.h +++ b/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.h @@ -32,4 +32,10 @@ */ @property (nonatomic, strong) NSDictionary* customAttributes; +/** + Additional user information. The value may be nil in case that server doesn't + provide such information. + */ +@property (nonatomic, strong) NSDictionary* userInfo; + @end diff --git a/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.m b/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.m index 2bc7b5f2..d946a556 100644 --- a/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.m +++ b/proj-xcode/PowerAuth2/private/PA2CreateActivationResponse.m @@ -26,6 +26,7 @@ - (instancetype) initWithDictionary:(NSDictionary *)dictionary NSDictionary * activationDataDict = PA2ObjectAs(dictionary[@"activationData"], NSDictionary); _activationData = [[PA2EncryptedResponse alloc] initWithDictionary:activationDataDict]; _customAttributes = PA2ObjectAs(dictionary[@"customAttributes"], NSDictionary); + _userInfo = PA2ObjectAs(dictionary[@"userInfo"], NSDictionary); } return self; } diff --git a/proj-xcode/PowerAuth2/private/PA2HttpRequest.m b/proj-xcode/PowerAuth2/private/PA2HttpRequest.m index 30838cc2..a635c07f 100644 --- a/proj-xcode/PowerAuth2/private/PA2HttpRequest.m +++ b/proj-xcode/PowerAuth2/private/PA2HttpRequest.m @@ -87,7 +87,10 @@ - (NSMutableURLRequest*) buildRequestWithHelper:(id)help } else { // Acquire encryptor from the helper, and keep it locally. // We will use it later, for the response decryption. - _encryptor = [helper encryptorWithId:_endpoint.encryptor]; + _encryptor = [helper encryptorWithId:_endpoint.encryptor error:error]; + if (!_encryptor) { + return nil; + } // Encrypt object PA2EncryptedRequest * encrypted = [PA2ObjectSerialization encryptObject:_requestObject encryptor:_encryptor diff --git a/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h b/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h index e73648b8..e18dcb15 100644 --- a/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h +++ b/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h @@ -32,7 +32,8 @@ /** Returns ECIES encryptor for given identifier. */ -- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId; +- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId + error:(NSError**)error; /** Calculates PowerAuth signature for data & endpoint. diff --git a/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.h b/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.h index b3cb2990..6d6305ca 100644 --- a/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.h +++ b/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.h @@ -99,6 +99,6 @@ typedef NS_ENUM(int, PA2EncryptorId) { The returned encryptor must not be reused, so the encryptor object has to be newly constructed. */ -- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId; +- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId error:(NSError**)error; @end diff --git a/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m b/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m index 745cabed..0b5d98bf 100644 --- a/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m +++ b/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m @@ -15,6 +15,8 @@ */ #import "PA2PrivateEncryptorFactory.h" +#import "PA2Result.h" +#import "PA2PrivateMacros.h" @import PowerAuthCore; @implementation PA2PrivateEncryptorFactory @@ -33,28 +35,31 @@ - (instancetype) initWithSessionProvider:(id)sessi return self; } -- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId +- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId error:(NSError**)error { switch (encryptorId) { // Generic case PA2EncryptorId_GenericApplicationScope: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Application sh1:@"/pa/generic/application" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Application sh1:@"/pa/generic/application" meta:YES error:error]; case PA2EncryptorId_GenericActivationScope: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/generic/activation" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/generic/activation" meta:YES error:error]; // Private case PA2EncryptorId_ActivationRequest: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Application sh1:@"/pa/generic/application" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Application sh1:@"/pa/generic/application" meta:YES error:error]; case PA2EncryptorId_ActivationPayload: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Application sh1:@"/pa/activation" meta:NO]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Application sh1:@"/pa/activation" meta:NO error:error]; case PA2EncryptorId_UpgradeStart: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/upgrade" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/upgrade" meta:YES error:error]; case PA2EncryptorId_VaultUnlock: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/vault/unlock" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/vault/unlock" meta:YES error:error]; case PA2EncryptorId_TokenCreate: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/token/create" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/token/create" meta:YES error:error]; case PA2EncryptorId_ConfirmRecoveryCode: - return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/recovery/confirm" meta:YES]; + return [self encryptorForScope:PowerAuthCoreEciesEncryptorScope_Activation sh1:@"/pa/recovery/confirm" meta:YES error:error]; default: + if (error) { + *error = PA2MakeError(PowerAuthErrorCode_Encryption, @"Unsupported encryptor"); + } return nil; } } @@ -64,14 +69,19 @@ - (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId - (PowerAuthCoreEciesEncryptor*) encryptorForScope:(PowerAuthCoreEciesEncryptorScope)scope sh1:(NSString*)sharedInfo1 meta:(BOOL)metaData + error:(NSError**)error { - return [_sessionProvider readTaskWithSession:^id _Nullable(PowerAuthCoreSession * _Nonnull session) { + return [[_sessionProvider readTaskWithSession:^PA2Result* _Nullable(PowerAuthCoreSession * _Nonnull session) { // Prepare data required for encryptor construction NSString * activationId = nil; PowerAuthCoreSignatureUnlockKeys * unlockKeys = nil; if (scope == PowerAuthCoreEciesEncryptorScope_Activation) { // For activation scope, also prepare activation ID and possession unlock key. activationId = session.activationIdentifier; + if (!activationId) { + PowerAuthErrorCode ec = session.hasPendingActivation ? PowerAuthErrorCode_ActivationPending : PowerAuthErrorCode_MissingActivation; + return [PA2Result failure:PA2MakeError(ec, nil)]; + } unlockKeys = [[PowerAuthCoreSignatureUnlockKeys alloc] init]; unlockKeys.possessionUnlockKey = _deviceRelatedKey; } @@ -82,13 +92,16 @@ - (PowerAuthCoreEciesEncryptor*) encryptorForScope:(PowerAuthCoreEciesEncryptorS PowerAuthCoreEciesEncryptor * encryptor = [session eciesEncryptorForScope:scope keys:unlockKeys sharedInfo1:sharedInfo1Data]; + if (!encryptor) { + return [PA2Result failure:PA2MakeError(PowerAuthErrorCode_Encryption, @"Failed to create ECIES encryptor")]; + } if (metaData) { // And assign the associated metadata encryptor.associatedMetaData = [[PowerAuthCoreEciesMetaData alloc] initWithApplicationKey:applicationKey activationIdentifier:activationId]; } - return encryptor; - }]; + return [PA2Result success:encryptor]; + }] extractResult:error]; } @end diff --git a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h index e155abf7..23b05089 100644 --- a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h +++ b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h @@ -72,4 +72,6 @@ + (instancetype) confirmRecoveryCode; ++ (instancetype) getUserInfo; + @end diff --git a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m index 2a97ded0..0165c622 100644 --- a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m +++ b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m @@ -130,6 +130,15 @@ + (instancetype) confirmRecoveryCode authUriId:@"/pa/recovery/confirm"]; } ++ (instancetype) getUserInfo +{ + return [[PA2RestApiEndpoint alloc] initWithPath:@"/pa/v3/user/info" + request:nil + response:[PowerAuthUserInfo class] + encryptor:PA2EncryptorId_GenericActivationScope + authUriId:nil]; +} + #pragma mark - Public getters - (BOOL) isEncrypted diff --git a/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h b/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h index 133c64b0..9d73e59b 100644 --- a/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h +++ b/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h @@ -34,3 +34,4 @@ #import "PA2EncryptedResponse.h" #import "PA2UpgradeStartV3Response.h" #import "PA2ConfirmRecoveryCodeResponse.h" +#import diff --git a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h index 3b1025f0..4af3f031 100644 --- a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h +++ b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h @@ -22,6 +22,7 @@ #import "PowerAuthActivationStatus+Private.h" #import "PowerAuthActivationCode+Private.h" #import "PowerAuthAuthentication+Private.h" +#import "PowerAuthUserInfo+Private.h" @import PowerAuthCore; @@ -46,6 +47,10 @@ - (PowerAuthCoreHTTPRequestDataSignature*) signHttpRequestData:(PowerAuthCoreHTTPRequestData*)requestData authentication:(PowerAuthAuthentication*)authentication error:(NSError**)error; +/** + Update last fetched user info. + */ +- (void) setLastFetchedUserInfo:(PowerAuthUserInfo*)lastFetchedUserInfo; @end diff --git a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m index 31e0a768..d7d196a3 100644 --- a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m +++ b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m @@ -22,12 +22,12 @@ @implementation PowerAuthSDK (CryptoHelper) -- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId +- (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId error:(NSError **)error { // The encryptors factory requires PowerAuthCoreSession & possesion unlock key for a proper operation. // After the enctyptor is created, we can destroy it. return [[[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:self.sessionProvider deviceRelatedKey:[self deviceRelatedKey]] - encryptorWithId:encryptorId]; + encryptorWithId:encryptorId error:error]; } - (PowerAuthAuthorizationHttpHeader*) authorizationHeaderForData:(NSData*)data diff --git a/proj-xcode/PowerAuth2/private/PowerAuthUserInfo+Private.h b/proj-xcode/PowerAuth2/private/PowerAuthUserInfo+Private.h new file mode 100644 index 00000000..01d18e20 --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PowerAuthUserInfo+Private.h @@ -0,0 +1,25 @@ + +/* + * 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. + */ + +#import +#import "PA2Codable.h" + +@interface PowerAuthUserInfo (Private) +@end + +@interface PowerAuthUserAddress (Private) +@end diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m index 7d276c40..6e67dae0 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m @@ -1281,4 +1281,32 @@ - (void) testWithWrongLAContext #endif // PA2_BIOMETRY_SUPPORT +#pragma mark - User Info + +- (void) testUserInfo +{ + CHECK_TEST_CONFIG(); + + PowerAuthSdkActivation * activation = [_helper createActivation:YES]; + if (!activation) { + return; + } + PowerAuthUserInfo * infoFromActivation = activation.activationResult.userInfo; + NSString * userId = _helper.testServerConfig.userIdentifier; + XCTAssertNotNil(_sdk.lastFetchedUserInfo); + XCTAssertNotNil(infoFromActivation); + XCTAssertEqualObjects(userId, _sdk.lastFetchedUserInfo.subject); + XCTAssertEqualObjects(userId, infoFromActivation.subject); + + PowerAuthUserInfo * info = [AsyncHelper synchronizeAsynchronousBlock:^(AsyncHelper *waiting) { + [_sdk fetchUserInfo:^(PowerAuthUserInfo * userInfo, NSError * error) { + XCTAssertNil(error); + [waiting reportCompletion:userInfo]; + }]; + }]; + XCTAssertNotNil(info); + XCTAssertEqualObjects(info.subject, _helper.testServerConfig.userIdentifier); + XCTAssertEqual(info, _sdk.lastFetchedUserInfo); +} + @end diff --git a/proj-xcode/PowerAuth2Tests/PowerAuthUserInfoTests.m b/proj-xcode/PowerAuth2Tests/PowerAuthUserInfoTests.m new file mode 100644 index 00000000..c305846e --- /dev/null +++ b/proj-xcode/PowerAuth2Tests/PowerAuthUserInfoTests.m @@ -0,0 +1,164 @@ +/* + * 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. + */ + +#import +@import PowerAuth2; + +#import "PowerAuthUserInfo+Private.h" + +@interface PowerAuthUserInfoTests : XCTestCase +@end + +@implementation PowerAuthUserInfoTests + +- (void) testEmptyObjectCreation +{ + PowerAuthUserInfo * info = [[PowerAuthUserInfo alloc] initWithDictionary:nil]; + XCTAssertNil(info); + PowerAuthUserAddress * address = [[PowerAuthUserAddress alloc] initWithDictionary:nil]; + XCTAssertNil(address); + + info = [[PowerAuthUserInfo alloc] initWithDictionary:@{}]; + XCTAssertNotNil(info); + XCTAssertNil(info.subject); + XCTAssertNil(info.name); + XCTAssertNil(info.givenName); + XCTAssertNil(info.familyName); + XCTAssertNil(info.middleName); + XCTAssertNil(info.nickname); + XCTAssertNil(info.preferredUsername); + XCTAssertNil(info.profileUrl); + XCTAssertNil(info.pictureUrl); + XCTAssertNil(info.websiteUrl); + XCTAssertNil(info.email); + XCTAssertFalse(info.isEmailVerified); + XCTAssertNil(info.phoneNumber); + XCTAssertFalse(info.isPhoneNumberVerified); + XCTAssertNil(info.gender); + XCTAssertNil(info.birthdate); + XCTAssertNil(info.zoneInfo); + XCTAssertNil(info.locale); + XCTAssertNil(info.address); + XCTAssertNil(info.updatedAt); + + address = [[PowerAuthUserAddress alloc] initWithDictionary:@{}]; + XCTAssertNotNil(address); + XCTAssertNil(address.formatted); + XCTAssertNil(address.street); + XCTAssertNil(address.locality); + XCTAssertNil(address.region); + XCTAssertNil(address.postalCode); + XCTAssertNil(address.country); +} + +- (void) testStandardClaims +{ + NSInteger timestamp = [[NSDate date] timeIntervalSince1970]; + NSMutableDictionary * claims = [@{ + @"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": @(YES), + @"gender": @"male", + @"birthdate": @"1984-02-21", + @"zoneinfo": @"Europe/Prague", + @"locale": @"en-US", + @"phone_number": @"+1 (425) 555-1212", + @"phone_number_verified": @(YES), + @"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": @(timestamp), + @"custom_claim": @"Hello world!" + } mutableCopy]; + + PowerAuthUserInfo * info = [[PowerAuthUserInfo alloc] initWithDictionary:claims]; + XCTAssertNotNil(info); + + XCTAssertEqualObjects(@"123456", info.subject); + XCTAssertEqualObjects(@"John Jacob Doe", info.name); + XCTAssertEqualObjects(@"John", info.givenName); + XCTAssertEqualObjects(@"Jacob", info.middleName); + XCTAssertEqualObjects(@"Doe", info.familyName); + XCTAssertEqualObjects(@"jjd", info.nickname); + XCTAssertEqualObjects(@"JacobTheGreat", info.preferredUsername); + XCTAssertEqualObjects(@"https://jjd.com/profile", info.profileUrl); + XCTAssertEqualObjects(@"https://jjd.com/avatar.jpg", info.pictureUrl); + XCTAssertEqualObjects(@"https://jjd.com", info.websiteUrl); + XCTAssertEqualObjects(@"jacob@jjd.com", info.email); + XCTAssertTrue(info.isEmailVerified); + XCTAssertEqualObjects(@"+1 (425) 555-1212", info.phoneNumber); + XCTAssertTrue(info.isPhoneNumberVerified); + XCTAssertEqualObjects(@"male", info.gender); + XCTAssertEqualObjects(@"Europe/Prague", info.zoneInfo); + XCTAssertEqualObjects(@"en-US", info.locale); + XCTAssertEqualObjects(@"Belehradska 858/23\n120 00 Prague - Vinohrady\nCzech Republic", info.address.formatted); + XCTAssertEqualObjects(@"Belehradska 858/23\nVinohrady", info.address.street); + XCTAssertEqualObjects(@"Prague", info.address.locality); + XCTAssertEqualObjects(@"Prague", info.address.region); + XCTAssertEqualObjects(@"12000", info.address.postalCode); + XCTAssertEqualObjects(@"Czech Republic", info.address.country); + XCTAssertEqualObjects(@"Hello world!", info.allClaims[@"custom_claim"]); + + XCTAssertEqualObjects(@(timestamp), @(info.updatedAt.timeIntervalSince1970)); + NSDateComponents * components = [[NSDateComponents alloc] init]; + components.day = 21; + components.month = 2; + components.year = 1984; + components.calendar = [NSCalendar currentCalendar]; + XCTAssertEqualObjects(components.date, info.birthdate); + + // Now alter some variables + + claims[@"phone_number_verified"] = @NO; + claims[@"email_verified"] = @NO; + claims[@"birthdate"] = @"1977-10-09"; + claims[@"middle_name"] = [NSNull null]; + + components.year = 1977; + components.month = 10; + components.day = 9; + + info = [[PowerAuthUserInfo alloc] initWithDictionary:claims]; + XCTAssertFalse(info.isEmailVerified); + XCTAssertFalse(info.isPhoneNumberVerified); + XCTAssertNil(info.middleName); + XCTAssertEqualObjects(components.date, info.birthdate); + + claims[@"birthdate"] = [NSNull null]; + claims[@"updated_at"] = [NSNull null]; + claims[@"address"] = [NSNull null]; + + info = [[PowerAuthUserInfo alloc] initWithDictionary:claims]; + XCTAssertNil(info.birthdate); + XCTAssertNil(info.updatedAt); + XCTAssertNil(info.address); +} + +@end