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
48 changes: 39 additions & 9 deletions api/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ components:
schemas:
ApplicationRequest:
type: object
required: [name, ouId]
required: [name, ouId, type]
properties:
ouId:
type: string
Expand Down Expand Up @@ -796,6 +796,13 @@ components:
format: uuid
description: The ID of the layout configuration associated with this application.
example: "770e8400-e29b-41d4-a716-446655440002"
type:
type: string
enum: [browser, fullstack, mobile, m2m, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
example: "browser"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
template:
type: string
description: The template type of the application.
Expand Down Expand Up @@ -859,10 +866,11 @@ components:
flowSecret:
type: string
description: >
Flow Secret for backend/server-side applications — non-public clients that do not use the
redirect-based authorization_code flow. Used to authenticate when initiating a flow directly
via the Flow Execution API. When omitted on creation, a secret is generated automatically for
eligible applications and returned once in the creation response.
Flow Secret used to authenticate when initiating a flow directly via the Flow Execution API.
Issued only to full-stack and custom applications that either have no OAuth 2.0 configuration
(embedded) or are configured as a confidential, non-redirect OAuth 2.0 client. Browser, mobile,
and machine-to-machine applications are never issued one. When omitted on creation, a secret is
generated automatically for eligible applications and returned once in the creation response.
example: "550e8400e29b41d4a716446655440000abcdef0123456789"

ApplicationCompleteResponse:
Expand Down Expand Up @@ -918,17 +926,25 @@ components:
format: uuid
description: The ID of the layout configuration associated with this application.
example: "660e8400-e29b-41d4-a716-446655440001"
type:
type: string
enum: [browser, fullstack, mobile, m2m, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
example: "browser"
Comment thread
Malith-19 marked this conversation as resolved.
template:
type: string
description: The template type of the application.
example: "spa"
flowSecret:
type: string
description: >
Flow Secret for backend/server-side applications. Returned only in the creation response
for eligible applications (non-public clients that do not use the redirect-based
authorization_code flow). Used to authenticate when initiating a flow directly via the
Flow Execution API.
Flow Secret used to authenticate when initiating a flow directly via the Flow Execution API.
Issued and returned once in the creation response only for full-stack and custom applications
that either have no OAuth 2.0 configuration (embedded) or are configured as a confidential,
non-redirect OAuth 2.0 client. Browser, mobile, and machine-to-machine applications are never
issued one.
example: "550e8400e29b41d4a716446655440000abcdef0123456789"
url:
type: string
Expand Down Expand Up @@ -1040,6 +1056,13 @@ components:
format: uuid
description: The ID of the layout configuration associated with this application.
example: "770e8400-e29b-41d4-a716-446655440002"
type:
type: string
enum: [browser, fullstack, mobile, m2m, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
example: "browser"
template:
type: string
description: The template type of the application.
Expand Down Expand Up @@ -1155,6 +1178,13 @@ components:
format: uuid
description: The ID of the layout configuration associated with this application.
example: "770e8400-e29b-41d4-a716-446655440002"
type:
type: string
enum: [browser, fullstack, mobile, m2m, custom]
description: >
The canonical application type (platform/client class). Required at creation and
immutable thereafter. Use custom for applications that do not fit the other classes.
example: "browser"
template:
type: string
description: The template type of the application.
Expand Down
1 change: 1 addition & 0 deletions backend/cmd/server/bootstrap/01-default-resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4981,6 +4981,7 @@ resource_type: application
id: 01900000-0000-7000-8000-000000000060
name: Console
description: Management application for ThunderID
type: browser
ouId: 01900000-0000-7000-8000-000000000001
url: "{{ .PUBLIC_URL }}/console"
logoUrl: "avatar:shape=rounded,variant=anonymous_entity,content=cube,colors=0,bg=#64be90"
Expand Down
156 changes: 156 additions & 0 deletions backend/internal/application/application_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package application

import (
"testing"

"github.com/stretchr/testify/suite"

"github.com/thunder-id/thunderid/internal/application/model"
inboundmodel "github.com/thunder-id/thunderid/internal/inboundclient/model"
"github.com/thunder-id/thunderid/pkg/thunderidengine/providers"
)

type ApplicationTypeTestSuite struct {
suite.Suite
}

func TestApplicationTypeTestSuite(t *testing.T) {
suite.Run(t, new(ApplicationTypeTestSuite))
}

// TestToInboundClientPersistsType verifies the application type is packed into the inbound client
// properties for persistence.
func (s *ApplicationTypeTestSuite) TestToInboundClientPersistsType() {
dto := &model.ApplicationProcessedDTO{ID: "app-1", Type: model.ApplicationTypeMobile}

dao := toInboundClient(dto)

s.Equal("mobile", dao.Properties[propType])
}

// TestToInboundClientOmitsEmptyType verifies an unset type is not written to properties.
func (s *ApplicationTypeTestSuite) TestToInboundClientOmitsEmptyType() {
dto := &model.ApplicationProcessedDTO{ID: "app-1"}

dao := toInboundClient(dto)

_, ok := dao.Properties[propType]
s.False(ok)
}

// TestToProcessedDTOReadsType verifies a persisted type is read back onto the DTO.
func (s *ApplicationTypeTestSuite) TestToProcessedDTOReadsType() {
dao := &inboundmodel.InboundClient{
ID: "app-1",
Properties: map[string]interface{}{propType: "browser"},
}

dto := toProcessedDTO(nil, dao, nil)

s.Equal(model.ApplicationTypeBrowser, dto.Type)
}

// TestToProcessedDTOEmptyWhenTypeAbsent verifies applications without a stored type resolve to an
// empty type (no implicit default is applied).
func (s *ApplicationTypeTestSuite) TestToProcessedDTOEmptyWhenTypeAbsent() {
withProps := toProcessedDTO(nil, &inboundmodel.InboundClient{
ID: "app-1",
Properties: map[string]interface{}{},
}, nil)
s.Equal(model.ApplicationType(""), withProps.Type)

nilProps := toProcessedDTO(nil, &inboundmodel.InboundClient{ID: "app-2"}, nil)
s.Equal(model.ApplicationType(""), nilProps.Type)
}

// TestBuildBasicApplicationResponseType verifies the list-view response reads the stored type and
// leaves it empty when absent (no implicit default).
func (s *ApplicationTypeTestSuite) TestBuildBasicApplicationResponseType() {
withType := buildBasicApplicationResponse(inboundmodel.InboundClient{
ID: "app-1",
Properties: map[string]interface{}{propType: "m2m"},
}, nil)
s.Equal(model.ApplicationTypeM2M, withType.Type)

absent := buildBasicApplicationResponse(inboundmodel.InboundClient{ID: "app-2"}, nil)
s.Equal(model.ApplicationType(""), absent.Type)
}

// TestFlowSecretIneligibleByType verifies browser, mobile, and m2m apps are never issued a Flow
// Secret, decided by their type alone regardless of OAuth config shape.
func (s *ApplicationTypeTestSuite) TestFlowSecretIneligibleByType() {
embedded := &providers.InboundAuthConfigWithSecret{
Type: providers.OAuthInboundAuthType,
OAuthConfig: &providers.OAuthConfigWithSecret{
GrantTypes: []providers.GrantType{
providers.GrantTypeClientCredentials,
providers.GrantTypeTokenExchange,
},
},
}
for _, appType := range []model.ApplicationType{
model.ApplicationTypeBrowser,
model.ApplicationTypeMobile,
model.ApplicationTypeM2M,
} {
s.False(isFlowSecretEligible(appType, nil), "type %q should not be eligible", appType)
s.False(isFlowSecretEligible(appType, embedded), "type %q should not be eligible", appType)
}
}

// TestFullStackAndCustomFlowSecretEligibility verifies full-stack and custom apps derive eligibility
// from the OAuth config shape: only confidential, non-redirect (embedded) clients are eligible.
func (s *ApplicationTypeTestSuite) TestFullStackAndCustomFlowSecretEligibility() {
embedded := &providers.InboundAuthConfigWithSecret{
Type: providers.OAuthInboundAuthType,
OAuthConfig: &providers.OAuthConfigWithSecret{
GrantTypes: []providers.GrantType{
providers.GrantTypeClientCredentials,
providers.GrantTypeTokenExchange,
},
TokenEndpointAuthMethod: providers.TokenEndpointAuthMethodClientSecretBasic,
},
}
redirect := &providers.InboundAuthConfigWithSecret{
Type: providers.OAuthInboundAuthType,
OAuthConfig: &providers.OAuthConfigWithSecret{
GrantTypes: []providers.GrantType{providers.GrantTypeAuthorizationCode},
},
}
m2mShaped := &providers.InboundAuthConfigWithSecret{
Type: providers.OAuthInboundAuthType,
OAuthConfig: &providers.OAuthConfigWithSecret{
GrantTypes: []providers.GrantType{providers.GrantTypeClientCredentials},
},
}

for _, appType := range []model.ApplicationType{
model.ApplicationTypeFullStack,
model.ApplicationTypeCustom,
} {
// Embedded app with no OAuth config, and confidential non-redirect app, are eligible.
s.True(isFlowSecretEligible(appType, nil), "type %q embedded should be eligible", appType)
s.True(isFlowSecretEligible(appType, embedded), "type %q embedded should be eligible", appType)
// Redirect and m2m-shaped apps are not eligible.
s.False(isFlowSecretEligible(appType, redirect), "type %q redirect should not be eligible", appType)
s.False(isFlowSecretEligible(appType, m2mShaped), "type %q m2m-shaped should not be eligible", appType)
}
}
1 change: 1 addition & 0 deletions backend/internal/application/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
propTosURI = "tos_uri"
propPolicyURI = "policy_uri"
propContacts = "contacts"
propType = "type"
propTemplate = "template"
propMetadata = "metadata"
propOAuthConfig = "oauth_config"
Expand Down
3 changes: 2 additions & 1 deletion backend/internal/application/declarative_resource.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down Expand Up @@ -191,6 +191,7 @@ func parseToApplicationDTO(data []byte) (*model.ApplicationDTO, error) {
AllowedUserTypes: appRequest.AllowedUserTypes,
LoginConsent: appRequest.LoginConsent,
},
Type: appRequest.Type,
Template: appRequest.Template,
FlowSecret: appRequest.FlowSecret,
URL: appRequest.URL,
Expand Down
44 changes: 43 additions & 1 deletion backend/internal/application/error_constants.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down Expand Up @@ -520,4 +520,46 @@ var (
"Both must point to the same {{param(flowType)}} flow.",
},
}
// ErrorInvalidApplicationType is returned when an application is created or updated with an
// unrecognized type value.
ErrorInvalidApplicationType = tidcommon.ServiceError{
Type: tidcommon.ClientErrorType,
Code: "APP-1040",
Error: tidcommon.I18nMessage{
Key: "error.applicationservice.invalid_application_type",
DefaultValue: "Invalid application type",
},
ErrorDescription: tidcommon.I18nMessage{
Key: "error.applicationservice.invalid_application_type_description",
DefaultValue: "The provided application type is not supported. It must be one of: " +
"browser, fullstack, mobile, m2m, custom.",
},
}
// ErrorApplicationTypeImmutable is returned when an update attempts to change the application type.
ErrorApplicationTypeImmutable = tidcommon.ServiceError{
Type: tidcommon.ClientErrorType,
Code: "APP-1041",
Error: tidcommon.I18nMessage{
Key: "error.applicationservice.application_type_immutable",
DefaultValue: "Application type cannot be changed",
},
ErrorDescription: tidcommon.I18nMessage{
Key: "error.applicationservice.application_type_immutable_description",
DefaultValue: "The application type is set at creation and cannot be modified.",
},
}
// ErrorApplicationTypeRequired is returned when an application is created without a type.
ErrorApplicationTypeRequired = tidcommon.ServiceError{
Type: tidcommon.ClientErrorType,
Code: "APP-1042",
Error: tidcommon.I18nMessage{
Key: "error.applicationservice.application_type_required",
DefaultValue: "Application type is required",
},
ErrorDescription: tidcommon.I18nMessage{
Key: "error.applicationservice.application_type_required_description",
DefaultValue: "An application type must be provided. It must be one of: " +
"browser, fullstack, mobile, m2m, custom.",
},
}
)
7 changes: 6 additions & 1 deletion backend/internal/application/handler.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
* Copyright (c) 2025-2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down Expand Up @@ -84,6 +84,7 @@ func (ah *applicationHandler) HandleApplicationPostRequest(w http.ResponseWriter
LoginConsent: appRequest.LoginConsent,
Attestation: appRequest.Attestation,
},
Type: appRequest.Type,
Template: appRequest.Template,
FlowSecret: appRequest.FlowSecret,
URL: appRequest.URL,
Expand Down Expand Up @@ -121,6 +122,7 @@ func (ah *applicationHandler) HandleApplicationPostRequest(w http.ResponseWriter
LoginConsent: createdAppDTO.LoginConsent,
Attestation: createdAppDTO.Attestation,
},
Type: createdAppDTO.Type,
Template: createdAppDTO.Template,
FlowSecret: createdAppDTO.FlowSecret,
URL: createdAppDTO.URL,
Expand Down Expand Up @@ -201,6 +203,7 @@ func (ah *applicationHandler) HandleApplicationGetRequest(w http.ResponseWriter,
LoginConsent: appDTO.LoginConsent,
Attestation: appDTO.Attestation,
},
Type: model.ApplicationType(appDTO.Type),
Template: appDTO.Template,
URL: appDTO.URL,
LogoURL: appDTO.LogoURL,
Expand Down Expand Up @@ -343,6 +346,7 @@ func (ah *applicationHandler) HandleApplicationPutRequest(w http.ResponseWriter,
LoginConsent: appRequest.LoginConsent,
Attestation: appRequest.Attestation,
},
Type: appRequest.Type,
Template: appRequest.Template,
FlowSecret: appRequest.FlowSecret,
URL: appRequest.URL,
Expand Down Expand Up @@ -380,6 +384,7 @@ func (ah *applicationHandler) HandleApplicationPutRequest(w http.ResponseWriter,
LoginConsent: updatedAppDTO.LoginConsent,
Attestation: updatedAppDTO.Attestation,
},
Type: updatedAppDTO.Type,
Template: updatedAppDTO.Template,
URL: updatedAppDTO.URL,
LogoURL: updatedAppDTO.LogoURL,
Expand Down
Loading
Loading