Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions auth_samples_android/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tuurio Auth Android Demo

An Android (Jetpack Compose) demo that signs in with OAuth 2.0 / OpenID Connect, then displays token contents and a logout button.
An Android (Jetpack Compose) demo that signs in with OAuth 2.0 / OpenID Connect, shows safe session metadata, and supports logout.

## Integration guide

Expand Down Expand Up @@ -34,8 +34,7 @@ Then run the `app` configuration on an emulator or device.
- A login screen with a “Continue with Tuurio ID” button.
- After you authenticate, you are redirected back to the app.
- The app shows:
- Access token and ID token (raw + decoded claims).
- Token expiry time and scope.
- Token expiry time and scope without rendering token values.
- UserInfo JSON (user profile).
- Logout button that ends the session and returns to the app.

Expand All @@ -47,12 +46,12 @@ Edit `app/src/main/java/com/tuurio/authsample/auth/AuthConfig.kt` with the value
https://<tenantId>.id.tuurio.com/admin/clients
```

The current sample values are:
Replace the placeholders with values for your own tenant and native client:

```
authorizeEndpoint: https://test.id.tuurio.com/oauth2/authorize
tokenEndpoint: https://test.id.tuurio.com/oauth2/token
clientId: spa-K53I
authorizeEndpoint: https://YOUR_TENANT.id.tuurio.com/oauth2/authorize
tokenEndpoint: https://YOUR_TENANT.id.tuurio.com/oauth2/token
clientId: YOUR_CLIENT_ID
redirectUri: com.example.app://oauth2redirect
scope: openid profile email
postLogoutRedirectUri: http://localhost:5173/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package com.tuurio.authsample.auth
import android.net.Uri

object AuthConfig {
val authorizeEndpoint: Uri = Uri.parse("https://test.id.tuurio.com/oauth2/authorize")
val tokenEndpoint: Uri = Uri.parse("https://test.id.tuurio.com/oauth2/token")
const val clientId: String = "spa-K53I"
val authorizeEndpoint: Uri = Uri.parse("https://your-tenant.id.tuurio.com/oauth2/authorize")
val tokenEndpoint: Uri = Uri.parse("https://your-tenant.id.tuurio.com/oauth2/token")
const val clientId: String = "replace-after-browser-handoff"
val redirectUri: Uri = Uri.parse("com.example.app://oauth2redirect")
const val scope: String = "openid profile email"

val discoveryUri: Uri = Uri.parse("https://test.id.tuurio.com/.well-known/openid-configuration")
val discoveryUri: Uri = Uri.parse("https://your-tenant.id.tuurio.com/.well-known/openid-configuration")
val postLogoutRedirectUri: Uri = Uri.parse("http://localhost:5173/")
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class AuthRepository(context: Context) {
if (responseCode >= 400) return null
val payload = connection.inputStream.bufferedReader().use { it.readText() }
val json = org.json.JSONObject(payload)
val endpoint = json.optString("userinfo_endpoint", null)
val endpoint = json.optString("userinfo_endpoint").takeIf { it.isNotBlank() }
userInfoEndpoint = endpoint
endpoint
} finally {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tuurio.authsample.auth

import java.text.DateFormat
import java.util.Date

fun formatTime(epochMillis: Long?): String {
if (epochMillis == null || epochMillis <= 0L) return "unknown time"
val formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
return formatter.format(Date(epochMillis))
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,18 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tuurio.authsample.auth.UserSession
import com.tuurio.authsample.auth.decodeJwt
import com.tuurio.authsample.auth.formatTime

@Immutable
Expand Down Expand Up @@ -122,12 +117,12 @@ private fun SidePanel(status: ShellStatus, modifier: Modifier) {
fontSize = 28.sp,
)
Text(
"A minimal Android client that signs in with OpenID Connect, displays decoded tokens, and supports secure logout redirects.",
"A minimal Android client that signs in with OpenID Connect and supports secure logout redirects.",
color = Color(0xFF64748B),
)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
StatusPill(status)
Text("Authority: test.id.tuurio.com", color = Color(0xFF64748B), fontSize = 13.sp)
Text("Authority: configured tenant", color = Color(0xFF64748B), fontSize = 13.sp)
}
}
}
Expand Down Expand Up @@ -251,7 +246,7 @@ fun LoginView(error: String?, onLogin: () -> Unit) {
) {
Text("Continue with Tuurio ID", color = Color.White, fontWeight = FontWeight.SemiBold)
}
Text("You'll be redirected to test.id.tuurio.com", color = Color(0xFF64748B), fontSize = 13.sp)
Text("You'll be redirected to your configured Tuurio tenant.", color = Color(0xFF64748B), fontSize = 13.sp)
}
if (!error.isNullOrBlank()) {
Spacer(modifier = Modifier.height(12.dp))
Expand Down Expand Up @@ -294,11 +289,6 @@ fun TokenView(session: UserSession, onLogout: () -> Unit) {
}
}

Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
TokenPanel("Access Token", session.accessToken, "Used to call protected APIs.")
TokenPanel("ID Token", session.idToken ?: "", "Proves the authenticated user.")
}

CardSurface(tone = CardTone.Soft) {
Text("User profile (UserInfo)", fontWeight = FontWeight.SemiBold)
CodeBlock(session.profileJson ?: "No profile data.")
Expand Down Expand Up @@ -326,39 +316,6 @@ private fun StatusMessage(message: String) {
}
}

@Composable
private fun TokenPanel(title: String, token: String, description: String) {
val clipboard = LocalClipboardManager.current
val copied = remember { mutableStateOf(false) }
val decoded = remember(token) { decodeJwt(token) }

CardSurface(tone = CardTone.Panel) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
Column(modifier = Modifier.weight(1f)) {
Text(title, fontWeight = FontWeight.SemiBold)
Text(description, color = Color(0xFF64748B), fontSize = 13.sp)
}
TextButton(onClick = {
clipboard.setText(androidx.compose.ui.text.AnnotatedString(token))
copied.value = true
}) {
Text(if (copied.value) "Copied" else "Copy")
}
}
CodeBlock(if (token.isBlank()) "Not provided" else token)
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Decoded claims", color = Color(0xFF0F766E), fontSize = 12.sp, letterSpacing = 1.4.sp)
CodeBlock(decoded ?: "Not a JWT or unable to decode.")
}
}
}
}

@Composable
private fun CodeBlock(content: String) {
Surface(
Expand Down
2 changes: 1 addition & 1 deletion auth_samples_android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
plugins {
id("com.android.application") version "8.4.2" apply false
id("com.android.application") version "8.13.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading