Skip to content

Credentials authentication

namnh-0652 edited this page Mar 9, 2023 · 6 revisions

Features

Installation

Prerequisites

  • Android 23+

Steps

From project build.gradle (or settings.gradle), add Jitpack maven

repositories {
    maven { url 'https://jitpack.io' }
}

Add these dependencies to your app/build.gradle

dependencies {
    implementation "com.github.sun-asterisk.tech-standard-android-auth:core:${latest_version}"
    implementation "com.github.sun-asterisk.tech-standard-android-auth:credentialsauth:${latest_version}"
}

Usage

SignIn with provided credentials

There are 3 steps for signIn with username & password using CredentialsAuth module.

Create your token model extends from AuthToken

data class Token(
    @Expose @SerializedName("id") val id: String? = null,
    @Expose @SerializedName("user_id") val userId: String? = null,
    @Expose @SerializedName("token") val accessToken: String? = null,
    @Expose @SerializedName("refresh_token") val refreshToken: String? = null,
    @Expose @SerializedName("expired_at") val expiresIn: String? = null
) : AuthToken {
    override val crAccessToken: String
        get() = accessToken.orEmpty() // required
    override var crRefreshToken: String? = null
        get() = refreshToken ?: field // required
}

Create configuration for CredentialsAuthConfig

From Application class, call initCredentialsAuth method

initCredentialsAuth(
    signInUrl = "http://10.0.5.78:8001/api/login", // replace YOUR_FULL_LOGIN_URL
    authTokenClazz = Token::class.java,
) {
    authTokenChanged = object : AuthTokenChanged<Token> {
        override fun onTokenUpdate(token: Token?): Request {
            return buildRefreshTokenRequest(token)
        }
    }
}

You can add more configurations, see CredentialsAuthConfig

Call your signIn API to get result.

fun signIn(username: String, password: String) {
    viewModelScope.launch(Dispatchers.IO) {
        CredentialsAuth.signIn(
            requestBody = SignInRequest(username, password), // change to your signin request body
            callback = object : AuthCallback<Token> {
                override fun onResult(data: Token?, error: Throwable?) {
                    _credentialsAuthResult.postValue(AuthResult(success = data, error = error))
                }
            }
        )
    }
}

Sequence diagram

sequenceDiagram
    autonumber
    participant Application
    participant CredentialsAuth
    participant AuthRepository
    note over CredentialsAuth, AuthRepository: CredentialsAuth Module
    participant Server
    Application->>CredentialsAuth: send SignIn() request
    CredentialsAuth->>AuthRepository: forward SignIn() request
    AuthRepository->>Server: call SignIn
    Server->>Server: Handle Request
    alt signIn process fails
        rect rgb(0, 0, 255, .1)
            break
                Server-->>AuthRepository: return error
                AuthRepository-->>CredentialsAuth: forward error
                CredentialsAuth-->>Application: Callback error
            end
        end
    else signIn Success
        Server-->>AuthRepository: return Token
        AuthRepository->>AuthRepository: save Token
        AuthRepository-->>CredentialsAuth: forward Token
        CredentialsAuth-->>Application: Callback Token
    end
Loading

Refresh token

Refresh token manually

Refresh token manually with 2 steps:

Create refresh token Request using OkHttp

import okhttp3.Request
// PATCH request body sample
val requestBody = MultipartBody.Builder().addPart(
    MultipartBody.Part.createFormData(
        "refresh_token",
        getToken()?.crRefreshToken.orEmpty() // to retrieve the last saved refresh token
    )
).build()

// PUT, POST request body sample
val json = JsonObject().apply { // using gson
    addProperty("refresh_token", getToken()?.crRefreshToken.orEmpty())
}
val requestBody = json.toString().toRequestBody(CredentialsAuth.JSON_MEDIA_TYPE)

val refreshTokenRequest = Request.Builder()
      .url("https://your.url/api/v1/auth_tokens") // replace YOUR_FULL_REFRESH_TOKEN_URL
      .put(requestBody)
    //.post(requestBody) 
    //.patch(requestBody)
      .build()

Call refresh token manually

fun refreshToken() {
    viewModelScope.launch(Dispatchers.IO) {
        CredentialsAuth.refreshToken(
            request = refreshTokenRequest,
            callback = object : AuthCallback<Token> {
                override fun onResult(data: Token?, error: Throwable?) {
                    _credentialsAuthResult.postValue(AuthResult(success = data, error = error))
                }
            }
        )
    }
}

Refresh token automatically using OkHttp

There are 2 steps to achieve this

Build refresh token request when token is updated via CredentialsAuthConfig, see create configurations

initCredentialsAuth(...) {
    ...
    authTokenChanged = object : AuthTokenChanged<Token> {
        override fun onTokenUpdate(token: Token?): Request {
            return buildRefreshTokenRequest(token)
        }
    }
}
fun buildRefreshTokenRequest(token: Token?): Request? {
    // See "create refresh token Request using OkHttp" above
}

Add TokenAuthenticator to your OkHttp client setting

client = OkHttpClient().newBuilder()
    .authenticator(TokenAuthenticator<Token>(REFRESH_TOKEN_EXPIRED_ERROR_CODE))
    .addInterceptor(...)
    ...
    .build()

Listen event when refreshToken is expired

When the refreshToken is expired, we can not use it anymore, user may need to re-signin again. To listen the event when refreshToken is expired, register BroadcastReceiver in your Activity or Fragment.

private val appIntentFilter = IntentFilter().apply {
    addAction(ACTION_REFRESH_TOKEN_EXPIRED) // need to listen this action
    // other actions
}
private val appBroadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
         when (intent?.action ?: return) {
             ACTION_REFRESH_TOKEN_EXPIRED -> {
                 // your logic here, For example: move to Login screen
             }
             // other actions
         }
    }
}
override fun onResume() {
    super.onResume()
    LocalBroadcastManager.getInstance(this).registerReceiver(appBroadcastReceiver, appIntentFilter)
}
override fun onPause() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(appBroadcastReceiver)
    super.onPause()
}

Sequence diagram

sequenceDiagram
    autonumber
    participant Application
    participant TokenAuthenticator
    note over TokenAuthenticator: CredentialsAuth Module
    participant Server
    Application->>Application: Setup OkHttp Client
    note over Application: add TokenAuthenticator <br> into OkHttp Client
    Application->>Server: send API Request(1)
    Server->>Server: Handle Request
    critical Token is expired error
        Server-->>TokenAuthenticator: 
        loop Util reaches refresh limit time <br> or refreshToken Success
            TokenAuthenticator->>TokenAuthenticator: get saved RefreshToken
            TokenAuthenticator->>Server: send refreshToken() request
            Server->>Server: Handle Request
            alt refreshToken Error 
                Server-->>TokenAuthenticator: return Error
            else refreshToken Success
                Server-->>TokenAuthenticator: return new Token
            end
        end
        TokenAuthenticator->>TokenAuthenticator: Save new Token
        TokenAuthenticator->>TokenAuthenticator: Update Token to API Request(1)
        TokenAuthenticator->>Server: Resend API Request(1)
        Server->>Server: Handle Request
        Server-->>Application: return response API Request(1)
    end
Loading

Local check you are logged in or not.

Just simply call this method

fun isSignedIn(): Boolean {
   return CredentialsAuth.isSignedIn<Token>()
}

Gets the current authentication token

Just simply call this method

fun getToken(): Token? {
    return CredentialsAuth.getToken()
}

Logout current user from local.

Just simply call this method

CredentialsAuth.logout {
  // Do your job when logout. Ex: clear other local data.
}

Documentation

Class diagram

classDiagram
    class CredentialsAuthConfig {
        
    }
    class CredentialsAuth {
        -AuthRepository repository
        -CredentialsAuthConfig config
    }
    class AuthRepository {
        <<interface>>
    }
    class AuthRepositoryImpl {
        -AuthRemoteDataSource remote
        -AuthLocalDataSource local
    }
    class AuthRemoteDataSource {
        -NonAuthApi api
    }
    class NonAuthApi {
        <<interface>>
    }
    class AuthLocalDataSource {
        -SharedPrefApi api
    }
    class SharedPrefApi {
        <<interface>>
    }
    CredentialsAuthConfig *-- CredentialsAuth
    AuthRepository *-- CredentialsAuth
    AuthRepositoryImpl --|> AuthRepository
    AuthRemoteDataSource *-- AuthRepositoryImpl
    AuthLocalDataSource *-- AuthRepositoryImpl
    NonAuthApi *--  AuthRemoteDataSource
    SharedPrefApi *--  AuthLocalDataSource
    SharedPrefApiImpl --|> SharedPrefApi
Loading
Method Purpose
suspend fun signIn(requestBody: Any?, callback: AuthCallback): Unit SignIn with given credentials
fun isSignedIn(): Boolean Local check user is signed in
fun signOut(doOnSignedOut: () -> Unit): Unit Local remove token
fun getToken(): T? Gets saved token
suspend fun refreshToken(request: Request, callback: AuthCallback?): Unit Manually refresh token
Method/field Purpose
signInUrl: String Full signIn URL with scheme & path
authTokenClazz: Class<*> Class which implements from AuthToken
authTokenChanged: AuthTokenChanged<*> Callback when Token is changed
basicAuthentication: String Sets authentication API basic authen
httpLogLevel: HttpLoggingInterceptor.Level Sets authentication API log level
connectTimeout: Long Sets authentication API connect timeout
readTimeout: Long Sets authentication API read timeout
writeTimeout: Long Sets authentication API write timeout
customHeaders: Headers? Sets authentication API Headers