Skip to content

Commit

Permalink
Added using ".well-known/openid-configuration" to load configuration.|
Browse files Browse the repository at this point in the history
  • Loading branch information
DenBond7 committed Aug 14, 2020
1 parent 36cf32b commit 149acfc
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import net.openid.appauth.ResponseTypeValues
* E-mail: [email protected]
*/
class OAuth2Helper {
enum class Provider constructor(val openidConfigurationUrl: String) {
MICROSOFT(OPENID_CONFIGURATION_URL_MICROSOFT)
}

companion object {
const val OPENID_CONFIGURATION_URL_MICROSOFT = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"

const val OAUTH2_GRANT_TYPE = "authorization_code"
const val OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token"

Expand All @@ -33,15 +39,12 @@ class OAuth2Helper {
*/
const val SCOPE_MICROSOFT_OAUTH2_FOR_MAIL = "openid offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send"

const val MICROSOFT_OAUTH2_AUTHORIZE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
const val MICROSOFT_OAUTH2_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
const val MICROSOFT_REDIRECT_URI = "msauth://com.flowcrypt.email.denys/04gM%2BEAfhnq4ALbhOX8jG5oRuow%3D"
const val MICROSOFT_AZURE_APP_ID = "3be51534-5f76-4970-9a34-40ef197aa018"
const val MICROSOFT_OAUTH2_SCHEMA = "msauth"

fun getMicrosoftAuthorizationRequest(loginHint: String? = null): AuthorizationRequest {
val configuration = AuthorizationServiceConfiguration(Uri.parse(MICROSOFT_OAUTH2_AUTHORIZE_URL), Uri.parse(MICROSOFT_OAUTH2_TOKEN_URL))

fun getMicrosoftAuthorizationRequest(loginHint: String? = null, configuration: AuthorizationServiceConfiguration): AuthorizationRequest {
val request = AuthorizationRequest.Builder(
configuration,
MICROSOFT_AZURE_APP_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.flowcrypt.email.api.retrofit.response.attester.PubResponse
import com.flowcrypt.email.api.retrofit.response.attester.TestWelcomeResponse
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.api.retrofit.response.oauth2.MicrosoftOAuth2TokenResponse
import com.google.gson.JsonObject

/**
* It's a repository interface for the whole API calls over the app
Expand Down Expand Up @@ -86,4 +87,10 @@ interface ApiRepository : BaseApiRepository {
* @param authorizeCode A code which will be used to retrieve an access token.
*/
suspend fun getMicrosoftOAuth2Token(requestCode: Long = 0L, context: Context, authorizeCode: String, scopes: String, codeVerifier: String): Result<MicrosoftOAuth2TokenResponse>

/**
* @param context Interface to global information about an application environment.
* @param url The configuration url.
*/
suspend fun getOpenIdConfiguration(requestCode: Long = 0L, context: Context, url: String): Result<JsonObject>
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.flowcrypt.email.api.retrofit.response.attester.LookUpEmailResponse
import com.flowcrypt.email.api.retrofit.response.attester.LookUpEmailsResponse
import com.flowcrypt.email.api.retrofit.response.attester.TestWelcomeResponse
import com.flowcrypt.email.api.retrofit.response.oauth2.MicrosoftOAuth2TokenResponse
import com.google.gson.JsonObject
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
Expand All @@ -30,6 +31,7 @@ import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Url

/**
* A base API interface for RETROFIT.
Expand Down Expand Up @@ -155,4 +157,7 @@ interface ApiService {
@Field("scope") scope: String = OAuth2Helper.SCOPE_MICROSOFT_OAUTH2_FOR_MAIL,
@Field("client_id") clientId: String = OAuth2Helper.MICROSOFT_AZURE_APP_ID,
@Field("grant_type") grant_type: String = OAuth2Helper.OAUTH2_GRANT_TYPE_REFRESH_TOKEN): Call<MicrosoftOAuth2TokenResponse>

@GET
suspend fun getOpenIdConfiguration(@Url url: String): Response<JsonObject>
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.flowcrypt.email.api.retrofit.response.attester.PubResponse
import com.flowcrypt.email.api.retrofit.response.attester.TestWelcomeResponse
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.api.retrofit.response.oauth2.MicrosoftOAuth2TokenResponse
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand Down Expand Up @@ -105,4 +106,12 @@ class FlowcryptApiRepository : ApiRepository {
apiService.getMicrosoftOAuth2Token(authorizeCode, scopes, codeVerifier)
}
}

override suspend fun getOpenIdConfiguration(requestCode: Long, context: Context, url: String): Result<JsonObject> =
withContext(Dispatchers.IO) {
val apiService = ApiHelper.getInstance(context).retrofit.create(ApiService::class.java)
getResult {
apiService.getOpenIdConfiguration(url)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ import com.flowcrypt.email.util.exception.ApiException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.AuthorizationServiceDiscovery
import org.jose4j.jwk.HttpsJwks
import org.jose4j.jwt.JwtClaims
import org.jose4j.jwt.consumer.JwtConsumerBuilder
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.util.*
import java.util.concurrent.TimeUnit

Expand All @@ -37,6 +43,34 @@ import java.util.concurrent.TimeUnit
class OAuth2AuthCredentialsViewModel(application: Application) : BaseAndroidViewModel(application) {
private val apiRepository: ApiRepository = FlowcryptApiRepository()
val microsoftOAuth2TokenLiveData = MutableLiveData<Result<AuthCredentials>>()
val authorizationRequestLiveData = MutableLiveData<Result<AuthorizationRequest>>()

fun getAuthorizationRequestForProvider(requestCode: Long = 0L, provider: OAuth2Helper.Provider) {
viewModelScope.launch {
authorizationRequestLiveData.postValue(Result.loading())

try {
val authRequest = when (provider) {
OAuth2Helper.Provider.MICROSOFT -> {
val jsonObject = getJsonObjectForOpenidConfiguration(requestCode, provider)
val authorizationServiceDiscovery = AuthorizationServiceDiscovery(jsonObject)
OAuth2Helper.getMicrosoftAuthorizationRequest(configuration = AuthorizationServiceConfiguration(authorizationServiceDiscovery))
}
}
authorizationRequestLiveData.postValue(Result.success(authRequest))
} catch (e: IOException) {
authorizationRequestLiveData.postValue(Result.exception(AuthorizationException.fromTemplate(AuthorizationException.GeneralErrors.NETWORK_ERROR, e)))
} catch (e: JSONException) {
authorizationRequestLiveData.postValue(Result.exception(AuthorizationException.fromTemplate(
AuthorizationException.GeneralErrors.JSON_DESERIALIZATION_ERROR, e)))
} catch (e: AuthorizationServiceDiscovery.MissingArgumentException) {
authorizationRequestLiveData.postValue(Result.exception(AuthorizationException.fromTemplate(
AuthorizationException.GeneralErrors.INVALID_DISCOVERY_DOCUMENT, e)))
} catch (e: Exception) {
authorizationRequestLiveData.postValue(Result.exception(e))
}
}
}

fun getMicrosoftOAuth2Token(requestCode: Long = 0L, authorizeCode: String, authRequest: AuthorizationRequest) {
viewModelScope.launch {
Expand Down Expand Up @@ -67,8 +101,12 @@ class OAuth2AuthCredentialsViewModel(application: Application) : BaseAndroidView
return@launch
}

val claims = validateTokenAndGetClaims(response.data?.idToken ?: "", authRequest
.clientId, jwks = JWKS_MICROSOFT)
val claims = validateTokenAndGetClaims(
idToken = response.data?.idToken ?: "",
clientId = authRequest.clientId,
jwks = authRequest.configuration.discoveryDoc?.jwksUri.toString()
)

val email: String? = claims.getClaimValueAsString(CLAIM_EMAIL)?.toLowerCase(Locale.US)
val displayName: String? = claims.getClaimValueAsString(CLAIM_NAME)

Expand Down Expand Up @@ -101,19 +139,38 @@ class OAuth2AuthCredentialsViewModel(application: Application) : BaseAndroidView
JwtClaims =
withContext(Dispatchers.IO) {
val httpsJkws = HttpsJwks(jwks)
val httpsJwksKeyResolver = HttpsJwksVerificationKeyResolver(httpsJkws)
val verificationKeyResolver = HttpsJwksVerificationKeyResolver(httpsJkws)
val jwtConsumer = JwtConsumerBuilder()
.setVerificationKeyResolver(httpsJwksKeyResolver)
.setVerificationKeyResolver(verificationKeyResolver)
.setExpectedAudience(clientId)
.build()
return@withContext jwtConsumer.processToClaims(idToken)
}

private suspend fun getJsonObjectForOpenidConfiguration(requestCode: Long, provider: OAuth2Helper.Provider): JSONObject =
withContext(Dispatchers.IO) {
val jsonObjectResult = apiRepository.getOpenIdConfiguration(
requestCode = requestCode,
context = getApplication(),
url = provider.openidConfigurationUrl
)

when (jsonObjectResult.status) {
Result.Status.SUCCESS -> {
return@withContext JSONObject(jsonObjectResult.data.toString())
}

Result.Status.EXCEPTION -> {
throw jsonObjectResult.exception ?: IOException("Couldn't fetch configurations")
}

else -> throw IOException("Couldn't fetch configurations")
}
}


companion object {
private const val CLAIM_EMAIL = "email"
private const val CLAIM_NAME = "name"

//https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
private const val JWKS_MICROSOFT = "https://login.microsoftonline.com/common/discovery/v2.0/keys"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Spinner
Expand Down Expand Up @@ -88,6 +89,7 @@ import kotlin.collections.ArrayList
class AddOtherAccountFragment : BaseSingInFragment(), ProgressBehaviour,
AdapterView.OnItemSelectedListener {

private var buttonSignInWithOutlook: Button? = null
private var editTextEmail: EditText? = null
private var editTextUserName: EditText? = null
private var editTextPassword: EditText? = null
Expand Down Expand Up @@ -414,14 +416,12 @@ class AddOtherAccountFragment : BaseSingInFragment(), ProgressBehaviour,
FeedbackActivity.show(requireActivity())
}

view.findViewById<View>(R.id.buttonSignInWithOutlook)?.setOnClickListener {
authRequest = OAuth2Helper.getMicrosoftAuthorizationRequest()
authRequest?.let { request ->
AuthorizationService(requireContext())
.performAuthorizationRequest(
request,
PendingIntent.getActivity(requireContext(), 0, Intent(requireContext(), SignInActivity::class.java), 0))
}
buttonSignInWithOutlook = view.findViewById(R.id.buttonSignInWithOutlook)
buttonSignInWithOutlook?.setOnClickListener {
it.isEnabled = false
oAuth2AuthCredentialsViewModel.getAuthorizationRequestForProvider(
requestCode = REQUEST_CODE_FETCH_MICROSOFT_OPENID_CONFIGURATION,
provider = OAuth2Helper.Provider.MICROSOFT)
}
}

Expand Down Expand Up @@ -604,6 +604,36 @@ class AddOtherAccountFragment : BaseSingInFragment(), ProgressBehaviour,
}

private fun setupOAuth2AuthCredentialsViewModel() {
oAuth2AuthCredentialsViewModel.authorizationRequestLiveData.observe(viewLifecycleOwner, Observer {
when (it.status) {
Result.Status.LOADING -> {
showProgress(progressMsg = getString(R.string.loading_oauth_server_configuration))
}

Result.Status.SUCCESS -> {
buttonSignInWithOutlook?.isEnabled = true
it.data?.let { authorizationRequest ->
authRequest = authorizationRequest
authRequest?.let { request ->
AuthorizationService(requireContext())
.performAuthorizationRequest(
request,
PendingIntent.getActivity(requireContext(), 0, Intent(requireContext(), SignInActivity::class.java), 0))
}
showContent()
}
}

Result.Status.ERROR, Result.Status.EXCEPTION -> {
buttonSignInWithOutlook?.isEnabled = true
showContent()
showInfoDialog(
dialogMsg = it.exception?.message ?: it.exception?.javaClass?.simpleName
?: "Couldn't load the server configuration")
}
}
})

oAuth2AuthCredentialsViewModel.microsoftOAuth2TokenLiveData.observe(viewLifecycleOwner, Observer {
when (it.status) {
Result.Status.LOADING -> {
Expand Down Expand Up @@ -872,6 +902,7 @@ class AddOtherAccountFragment : BaseSingInFragment(), ProgressBehaviour,
private const val REQUEST_CODE_ADD_NEW_ACCOUNT = 10
private const val REQUEST_CODE_CHECK_PRIVATE_KEYS_FROM_EMAIL = 11
private const val REQUEST_CODE_RETRY_SETTINGS_CHECKING = 12
private const val REQUEST_CODE_FETCH_MICROSOFT_OPENID_CONFIGURATION = 13L

fun newInstance(): AddOtherAccountFragment {
return AddOtherAccountFragment()
Expand Down
1 change: 1 addition & 0 deletions FlowCrypt/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -469,4 +469,5 @@
<string name="could_not_verify_response">Sorry, we couldn\'t verify the response. Please try again or write to us at %1$s</string>
<string name="error_with_value">Error: %1$s</string>
<string name="oauth_error">OAuth error</string>
<string name="loading_oauth_server_configuration">Loading OAuth server configuration&#8230;</string>
</resources>

0 comments on commit 149acfc

Please sign in to comment.