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
15 changes: 11 additions & 4 deletions api/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1435,15 +1435,22 @@ components:
type: object
description: >
Platform attestation configuration used to verify the binary identity of a mobile client
when it initiates a flow directly over HTTP. Configure exactly one platform.
oneOf:
- required: [android]
- required: [apple]
when it initiates a flow directly over HTTP. Configure at most one platform; devMode is
independent of the platform fields and may be set with neither configured.
not:
required: [android, apple]
properties:
android:
$ref: '#/components/schemas/AndroidAttestation'
apple:
$ref: '#/components/schemas/AppleAttestation'
devMode:
type: boolean
default: false
description: >
When true, skips the platform-attestation check for a mobile application during direct
flow initiation. Disabled by default; enable only for testing or trying out
sample/development mobile clients.

AndroidAttestation:
type: object
Expand Down
6 changes: 5 additions & 1 deletion backend/internal/application/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1757,7 +1757,11 @@ func (as *applicationService) resolveAttestationCredentialsForPersist(
}
}

inboundClient.Attestation = &providers.AttestationConfig{Android: &android, Apple: inboundClient.Attestation.Apple}
inboundClient.Attestation = &providers.AttestationConfig{
Android: &android,
Apple: inboundClient.Attestation.Apple,
DevMode: inboundClient.Attestation.DevMode,
}
return nil
}

Expand Down
24 changes: 24 additions & 0 deletions backend/internal/application/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1833,6 +1833,30 @@ func (suite *ServiceTestSuite) TestResolveAttestationCredentials_PreservesExisti
assert.Equal(suite.T(), "com.example.app", inboundClient.Attestation.Android.PackageName)
}

// The Android credential rebuild must not drop a DevMode setting configured alongside it.
func (suite *ServiceTestSuite) TestResolveAttestationCredentials_PreservesDevMode() {
service, mockStore := suite.setupTestService()

const appID = "app-1"
mockStore.On("GetInboundClientByEntityID", mock.Anything, appID).Return(
&inboundmodel.InboundClient{
Attestation: &providers.AttestationConfig{
Android: &providers.AndroidAttestationConfig{ServiceAccountCredentials: "stored-encrypted"},
},
}, nil)

inboundClient := &inboundmodel.InboundClient{
Attestation: &providers.AttestationConfig{
Android: &providers.AndroidAttestationConfig{PackageName: "com.example.app"},
DevMode: true,
},
}

svcErr := service.resolveAttestationCredentialsForPersist(context.Background(), appID, inboundClient)
require.Nil(suite.T(), svcErr)
assert.True(suite.T(), inboundClient.Attestation.DevMode)
}

// A non-"not found" lookup failure while preserving omitted credentials is propagated as an internal
// error, so a transient store failure cannot silently overwrite stored credentials with an empty
// value.
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/flow/flowexec/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ const (
// identity. This takes precedence over the redirect-based classification for apps that configure
// attestation.
flowInitiationAttestation
// flowInitiationDevMode indicates a mobile application with attestation dev mode enabled, which may
// initiate a flow directly without presenting a platform attestation. Intended for testing and
// trying out sample or development mobile clients; disabled by default.
flowInitiationDevMode
)
12 changes: 11 additions & 1 deletion backend/internal/flow/flowexec/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ func (s *flowExecService) loadNewContext(ctx context.Context, appID, flowTypeStr
// profile) that must authenticate at flow initiation by presenting its Flow Secret.
// - Attestation — a mobile application that authenticates at flow initiation by presenting a valid
// platform attestation (e.g. a Google Play Integrity token) proving its binary identity.
// - DevMode — a mobile application with attestation dev mode enabled, which may initiate a flow
// without presenting an attestation. Disabled by default; intended for testing and trying out
// sample/development mobile clients.
//
// Sign-out is guarded like authentication so a native caller must prove its identity before ending a
// session; a redirect-based app is pushed to the RP-initiated /oauth2/logout endpoint instead. Other
Expand Down Expand Up @@ -266,6 +269,8 @@ func (s *flowExecService) checkDirectFlowInitiationAllowed(ctx context.Context,
return nil
case flowInitiationAttestation:
return s.verifyAttestation(ctx, attestationCfg, attestationToken)
case flowInitiationDevMode:
return nil
default:
logger.Error(ctx, "Unknown flow initiation mode for application",
log.String("appID", appID))
Expand Down Expand Up @@ -309,7 +314,12 @@ func (s *flowExecService) resolveFlowInitiationMode(
// M2M apps get tokens directly; browser apps are public redirect clients. Neither runs flows.
return flowInitiationNotPermitted, nil, nil
case appmodel.ApplicationTypeMobile:
// Mobile apps authenticate with platform attestation, which must be configured first.
// Dev mode lets a mobile app initiate flows without a platform attestation, for testing or
// trying out sample/development clients. Disabled by default.
if client.Attestation != nil && client.Attestation.DevMode {
return flowInitiationDevMode, nil, nil
}
// Otherwise, mobile apps authenticate with platform attestation, which must be configured first.
if client.Attestation == nil || (client.Attestation.Android == nil && client.Attestation.Apple == nil) {
return 0, nil, &ErrorAttestationNotConfigured
}
Expand Down
29 changes: 29 additions & 0 deletions backend/internal/flow/flowexec/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2431,6 +2431,11 @@ func (s *ServiceTestSuite) TestResolveFlowInitiationMode_ByType() {
attestation: &providers.AttestationConfig{Apple: &providers.AppleAttestationConfig{}},
expectMode: flowInitiationAttestation,
},
{
name: "mobile with dev mode skips attestation", appType: model.ApplicationTypeMobile,
attestation: &providers.AttestationConfig{DevMode: true},
expectMode: flowInitiationDevMode,
},
{
name: "mcp embedded uses flow secret", appType: model.ApplicationTypeMCP,
profile: &providers.OAuthProfile{GrantTypes: []string{"client_credentials", tokenExchange}},
Expand Down Expand Up @@ -3058,6 +3063,30 @@ func (s *ServiceTestSuite) TestCheckDirectFlowInitiationAllowed_AppleAttestation
s.Nil(svcErr)
}

// A mobile app with attestation dev mode enabled may initiate a flow directly without presenting an
// attestation token, and no verification is attempted.
func (s *ServiceTestSuite) TestCheckDirectFlowInitiationAllowed_DevModeSkipsAttestation() {
t := s.T()
mockActorProvider := actorprovidermock.NewActorProviderMock(t)
devModeClient := &providers.InboundClient{
ID: "mobile-app",
Properties: map[string]interface{}{
applicationTypePropertyKey: string(model.ApplicationTypeMobile),
},
Attestation: &providers.AttestationConfig{DevMode: true},
}
mockActorProvider.EXPECT().GetInboundClientByID(mock.Anything, "mobile-app").Return(devModeClient, nil)

service := &flowExecService{
actorProvider: mockActorProvider,
cfg: testFlowExecCfg,
}

svcErr := service.checkDirectFlowInitiationAllowed(context.Background(), "mobile-app",
providers.FlowTypeAuthentication, "", "", log.GetLogger())
s.Nil(svcErr)
}

// --- getFlowContext ---

func (s *ServiceTestSuite) TestGetFlowContext_NilDbModel() {
Expand Down
3 changes: 2 additions & 1 deletion backend/pkg/thunderidengine/providers/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ type Certificate struct {
type AttestationConfig struct {
Android *AndroidAttestationConfig `json:"android,omitempty" yaml:"android,omitempty" jsonschema:"Google Play Integrity attestation configuration for Android clients."`
Apple *AppleAttestationConfig `json:"apple,omitempty" yaml:"apple,omitempty" jsonschema:"Apple App Attest attestation configuration for iOS clients."`
DevMode bool `json:"devMode,omitempty" yaml:"devMode,omitempty" jsonschema:"When true, skips the platform-attestation check for a mobile application during direct flow initiation. Disabled by default; enable only for testing or trying out sample/development mobile clients."`
}

// AndroidAttestationConfig holds the Google Play Integrity settings for an Android application.
Expand All @@ -656,7 +657,7 @@ func (c *AttestationConfig) WithoutCredentials() *AttestationConfig {
if c == nil {
return nil
}
sanitized := &AttestationConfig{}
sanitized := &AttestationConfig{DevMode: c.DevMode}
if c.Android != nil {
android := *c.Android
android.ServiceAccountCredentials = ""
Expand Down
8 changes: 8 additions & 0 deletions backend/pkg/thunderidengine/providers/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,11 @@ func (suite *ModelTestSuite) TestAttestationConfig_WithoutCredentials_PassesAppl
assert.Equal(suite.T(), "com.example.app", sanitized.Apple.BundleID)
assert.Nil(suite.T(), sanitized.Android)
}

func (suite *ModelTestSuite) TestAttestationConfig_WithoutCredentials_PassesDevModeThrough() {
cfg := &AttestationConfig{DevMode: true}

sanitized := cfg.WithoutCredentials()

assert.True(suite.T(), sanitized.DevMode)
}
8 changes: 8 additions & 0 deletions docs/content/guides/applications/application-settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ attestation:
```
:::

### Dev Mode

Configuring `attestation.android` or `attestation.apple` requires a signed build with the exact package name, signing certificate, or bundle identifier registered above. A sample application or a local development build usually cannot produce a valid attestation. **Dev Mode** lets a Mobile application initiate a flow directly without presenting an attestation token, regardless of whether an Android or iOS platform is configured. Enable it from the toggle in the **Platform Attestation** card header, or set `attestation.devMode` to `true` in a declarative resource. **Dev Mode** is disabled by default.

:::warning
**Dev Mode** skips the platform-attestation check only when this application initiates a flow directly. It does not affect any other application operation. Use it only for testing, or to try out a sample or development client. Disable it before the application goes to production.
:::

## Related Guides

- [Manage Applications](../manage-applications) - Create, update, and delete applications
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ Attestation-Token: <attestation-token>

A Mobile application that has not configured attestation is rejected with `400 Bad Request`. Once attestation is configured, an application that omits the token is rejected with `401 Unauthorized`, and an invalid or malformed token is also rejected with `401 Unauthorized`. Flow **continuation** requests (those carrying an `executionId`) do not require an attestation token.

:::note
A Mobile application with **Dev Mode** enabled is an exception to the rejections above. It can initiate a flow without presenting an attestation token, regardless of whether a platform is configured. **Dev Mode** is intended for testing and trying out sample or development clients, not production use. See [Dev Mode](../../guides/applications/application-settings.mdx#dev-mode).
:::

### Integration Modes

App-native authentication supports two modes, **Verbose** and **Non-Verbose**, controlled via the `verbose` field in the Flow Execution API. These modes offer different levels of UI granularity to the application. A <ProductName /> SDK typically consumes these modes and manages API calls, state, and response parsing. Both modes follow the same request-response cycle: the application calls the Flow Execution API, receives the next step, renders the screen, collects user input, and submits the response back. The difference lies in how much detail <ProductName /> returns for each step.
Expand Down
Loading
Loading