Concepts: Learn about how Sign in with Email/Phone Number works.
Verifying email and phone number in your mobile app using our Authentication API consists of the following steps:
Open a WebView within your app with shared cookies
Direct users to Cotter's Auth page
Redirect back to your app with an authorization code
Call Cotter API with the authorization code
Get back the user's email or phone number, and whether or not it's verified
Here's an example on opening the in-app Browser from iOS and Android
Android: Use the Trusted Web Activity
iOS: Use the ASWebAuthenticationSession
For mobile apps, we're going to use the OAuth 2.0 Authorization Code Flow with Proof Key for Code Exchange (PKCE). This flow is recommended for Mobile Apps because:
Mobile apps can't securely store the Secret Key. This is because decompiling the App will reveal the Secret Key, and there's only one secret key so it'll be the same for all users.
Sending tokens to Custom URL schemes (ex. YourApp://) will potentially expose the tokens to malicious apps.
Create a code_verifier
and a code_challenge
Request Authorization from Cotter: Redirect user to Cotter to verify their email/phone and receive an authorization_code
back to your app.
Request Tokens and Identity: Send your authorization_code
and code_verifier
to Cotter server and get back a token
and the user's email or phone number.
Include Token to your server: The token contains the user's verified email/phone number and a signature. Include this to your signup/login request to your backend
A code_verifier
is a cryptographically-random key that will be sent to Cotter along with the authorization_code
on Step 3. Read more about what are code challenge and verifier.
function dec2hex(dec) {return ('0' + dec.toString(16)).substr(-2)}function generateRandomString() {var array = new Uint32Array(56/2);window.crypto.getRandomValues(array);return Array.from(array, dec2hex).join('');}var verifier = generateRandomString();
import osimport base64verifier_bytes = os.urandom(32)code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b'=')
// import android.util.Base64;SecureRandom sr = new SecureRandom();byte[] code = new byte[32];sr.nextBytes(code);String verifier = Base64.encodeToString(code, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
var buffer = [UInt8](repeating: 0, count: 32)_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)let verifier = Data(bytes: buffer).base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "\_").replacingOccurrences(of: "=", with: "").trimmingCharacters(in: .whitespaces)
NSMutableData *data = [NSMutableData dataWithLength:32];int result __attribute__((unused)) = SecRandomCopyBytes(kSecRandomDefault, 32, data.mutableBytes);NSString *verifier = [[[[data base64EncodedStringWithOptions:0]stringByReplacingOccurrencesOfString:@"+" withString:@"-"]stringByReplacingOccurrencesOfString:@"/" withString:@"_"]stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
A code_challenge
is the hashed version of your code_verifier
. We will send this hash on step 2 when you're requesting an authentication from Cotter.
function sha256(plain) { // returns promise ArrayBufferconst encoder = new TextEncoder();const data = encoder.encode(plain);return window.crypto.subtle.digest('SHA-256', data);}function base64urlencode(a) {var str = "";var bytes = new Uint8Array(a);var len = bytes.byteLength;for (var i = 0; i < len; i++) {str += String.fromCharCode(bytes[i]);}return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");}async function challenge_from_verifier(v) {hashed = await sha256(v);base64encoded = base64urlencode(hashed);return base64encoded;}var challenge = await challenge_from_verifier(verifier);
import hashlibimport base64challenge_bytes = hashlib.sha256(code_verifier).digest()code_challenge = base64.urlsafe_b64encode(challenge_bytes).rstrip(b'=')
// import android.util.Base64;byte[] codeVerifierBytes = codeVerifier.getBytes("US-ASCII");MessageDigest md = MessageDigest.getInstance("SHA-256");md.update(codeVerifierBytes);byte[] codeChallengeBytes = md.digest();String codeChallenge = Base64.encodeToString(codeChallengeBytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
// Dependency: Apple Common Crypto library// http://opensource.apple.com//source/CommonCryptoguard let data = verifier.data(using: .utf8) else { return nil }var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))data.withUnsafeBytes {_ = CC_SHA256($0, CC_LONG(data.count), &buffer)}let hash = Data(bytes: buffer)let challenge = hash.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "\_").replacingOccurrences(of: "=", with: "").trimmingCharacters(in: .whitespaces)
// Dependency: Apple Common Crypto library// http://opensource.apple.com//source/CommonCryptou_int8_t buffer[CC_SHA256_DIGEST_LENGTH * sizeof(u_int8_t)];memset(buffer, 0x0, CC_SHA256_DIGEST_LENGTH);NSData *data = [verifier dataUsingEncoding:NSUTF8StringEncoding];CC_SHA256([data bytes], (CC_LONG)[data length], buffer);NSData *hash = [NSData dataWithBytes:buffer length:CC_SHA256_DIGEST_LENGTH];NSString *challenge = [[[[hash base64EncodedStringWithOptions:0]stringByReplacingOccurrencesOfString:@"+" withString:@"-"]stringByReplacingOccurrencesOfString:@"/" withString:@"_"]stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
The code_challenge
is sent first so that later in step 3, Cotter's server can verify that hash(code_verifier)
is the same as code_challenge
and that you are indeed made the original request.
To check if your code_challenge
and code_verifier
are correctly generated and formatted, try comparing it with codes generated here https://example-app.com/pkce
Open Cotter's Auth URL from a WebView from your app.
https://js.cotter.app/app?api_key=<api_key_id>&redirect_url=yourapp://&type=PHONE&code_challenge=<code_challenge>&state=<state>
Query Parameter | Type | Description |
| string | Your |
| string | Your app's URL scheme where Cotter Auth will redirect back your users to your app Example: |
| string |
|
| string | The |
| string | A random string that you generate from your application before opening to Cotter's Auth (ex. |
Make sure the scheme of your redirect_url
(the front part before ://
) doesn't have an underscore or other special characters. To test it out, enter your redirect_url
here: https://jsfiddle.net/omd02jn5/
Here's an example on opening the in-app Browser from iOS and Android
Android: Use the Trusted Web Activity
iOS: Use the ASWebAuthenticationSession
// Using ASWebAuthenticationSession// https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_serviceguard let authURL = URL(string: "https://js.cotter.app/app?api_key=<api_key_id>&redirect_url=yourapp://&type=PHONE&code_challenge=<code_challenge>&state=<state>") else { return }let scheme = "yourapp://"self.authSession = ASWebAuthenticationSession(url: authURL, callbackURLScheme: scheme){ callbackURL, error in// Handle the callback.}if #available(iOS 13.0, *) {self.authSession?.presentationContextProvider = self} else {// Fallback on earlier versions}self.authSession?.start()
// Dependencies: Trusted Web Activity// https://developers.google.com/web/updates/2019/02/using-twa//build.gradle (Module:app)android {...compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}}dependencies {implementation 'com.google.androidbrowserhelper:androidbrowserhelper:1.0.0'}// MainActivity.javaimport com.google.androidbrowserhelper.trusted.TwaLauncher;public class MainActivity extends AppCompatActivity {...static Uri LAUNCH_URI = Uri.parse("https://js.cotter.app/app?api_key=<api_key_id>&redirect_url=yourapp://&type=PHONE&code_challenge=<code_challenge>&state=<state>");public void login(View view) {new TwaLauncher(this).launch(LAUNCH_URI);}}
Open the user's browser with the URL above, also display it so the user can click it if opening the browser doesn't work.
Listen to the localhost port that you specified in the redirect URL
When the browser redirect back to you with the response below, handle the request and use the code, state, and challenge_id to continue to Step 3.
After the user's email or phone is verified, Cotter will redirect back to your app using redirect_url
that you specified in step 2.
yourapp://?code=<authorization_code>&state=<state>&challenge_id=<challenge_id>
You should check that the state
is the same as the initial state you passed in to the URL here.
In this step, you'll use your code_verifier
, authorization_code
and the challenge_id
to request tokens
and the user's email or phone number from Cotter's server.
Your authorization_token
is valid for 5 minutes, and can only be used once.
curl -XPOST \-H 'Content-type: application/json' \-H 'API_KEY_ID: <api_key_id>' \-d '{"code_verifier": "<code_verifier>","authorization_code": "<authorization_code>","challenge_id": <challenge_id>,"redirect_url": "<redirect_url>"}' 'https://www.cotter.app/api/v0/verify/get_identity?oauth_token=true'
API_KEY_ID
application/json
true
, will return OAuth Tokens (read "Handling Authentication with Cotter")code_verifier
created in Step 1authorization_code
received in Step 2challenge_id
received in Step 2redirect_url
you specified in Step 2identifier
and a token
which contains a signature that you need to verify.{"identifier": {"ID": "f4286df9-a923-429c-bc33-5089ffed5f68","created_at": "2020-07-21T22:53:21.211367Z","updated_at": "2020-07-21T22:53:21.211367Z","deleted_at": "0001-01-01T00:00:00Z","identifier": "putri@cotter.app", // User's email"identifier_type": "EMAIL","device_type": "BROWSER","device_name": "Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36","expiry": "2020-08-20T22:53:21.19705Z","timestamp": "2020-07-21T22:53:21.19705Z"},"oauth_token": {"access_token": "eyJhbGciOiJFUz...", // Validate this access token"id_token": "eyJhbGciOiJFUzI1...","refresh_token": "27944:lb31DY5pG229n...","expires_in": 3600,"token_type": "Bearer","auth_method": "OTP"},"token": {...},"user": {"ID": "643a42c7-316a-4abe-b27e-f4d0f903bfea", // Cotter uesr ID"identifier": "putri@cotter.app",...}}
authorization_token
is expired (you have 5 minutes to use the token), or you already used the token once.{"msg": "Challenge Expired"}
Now that the email or phone number is verified, you can continue your Sign Up or Login process by submitting the email or phone number to your server, either now or after the user enters more information.
You should include this oauth_tokens
into your call to your backend for Login or Registration. Your backend should then verify that the access token is valid.
Check out how to verify the OAuth Tokens from Cotter here:
Since you'll be using your API Key from a front-end website or mobile app, your API_KEY_ID
is exposed to anyone inspecting your code. Here are some ways to prevent abuse:
Your app generates state=XYZ
in the beginning of the auth flow. You should expect that Cotter's response on Step 2 when Cotter redirect back to your redirect_url
, the state is the same (state == XYZ
). This makes sure that the redirect was in response to your initial authentication request.
This is needed for installed apps / SPA because they cannot store the Api Secret Key securely, so the code_challenge
and code_verifier
is for Cotter to make sure that the original App that requested authentication on Step 2 is the same as the one that asked for access token on Step 3.